prometheus.summary

Run summary builder and emitter.

This module contains the logic to construct and emit the end-of-run summary for a Prometheus run. It intentionally operates on a Prometheus instance passed in to avoid circular imports.

Functions:

  • emit_run_summary

    Build and emit a run summary for the given Prometheus instance.

emit_run_summary

emit_run_summary(prom, outfile, end_out, size=None)

Build and emit a run summary for the given Prometheus instance.

Parameters:
  • prom (Prometheus) –

    The Prometheus instance (used only to read attributes; not imported).

  • outfile (str) –

    Path to the output file written by the run.

  • end_out (float) –

    Timestamp when the output write finished (seconds since epoch).

  • size (int or None, default: None ) –

    Size in bytes of the output file if already known; if None the function will attempt to stat the file.

Source code in prometheus/summary.py
def emit_run_summary(prom, outfile, end_out, size=None):
    """Build and emit a run summary for the given Prometheus instance.

    Parameters
    ----------
    prom : Prometheus
        The Prometheus instance (used only to read attributes; not imported).
    outfile : str
        Path to the output file written by the run.
    end_out : float
        Timestamp when the output write finished (seconds since epoch).
    size : int or None
        Size in bytes of the output file if already known; if None the
        function will attempt to stat the file.
    """
    logger = logging.getLogger("prometheus.prometheus")

    try:
        try:
            events_written = len(prom.injection) if prom.injection is not None else 0
        except Exception:
            events_written = 0

        inj_time = float(
            getattr(prom, "_end_inj", float("nan")) - getattr(prom, "_start_inj", float("nan"))
        )
        prop_time = float(
            getattr(prom, "_end_prop", float("nan")) - getattr(prom, "_start_prop", float("nan"))
        )
        write_time = float(
            getattr(prom, "_end_out", end_out) - getattr(prom, "_start_out", float("nan"))
        )
        total_time = float(
            getattr(prom, "_end_out", end_out) - getattr(prom, "_run_start_time", float("nan"))
        )
        throughput = events_written / total_time if total_time and total_time > 0 else float("nan")

        start_ts = datetime.fromtimestamp(getattr(prom, "_run_start_time", time())).isoformat()
        end_ts = datetime.fromtimestamp(end_out).isoformat()

        logfile = getattr(config.run, "logfile", None) or "console"

        # External subprocess statuses (ppc)
        external_statuses = []
        try:
            from .photon_propagation import ppc_photon_propagator as _ppc_mod

            external_statuses = getattr(_ppc_mod, "subprocess_statuses", [])
        except Exception:
            external_statuses = []

        warn = err = crit = 0
        if getattr(prom, "_log_counter", None) is not None:
            warn = getattr(prom._log_counter, "warning_count", 0)
            err = getattr(prom._log_counter, "error_count", 0)
            crit = getattr(prom._log_counter, "critical_count", 0)
            getattr(prom._log_counter, "info_count", 0)

        # File size and checksum
        if size is None:
            try:
                size = Path(outfile).stat().st_size
            except Exception:
                size = None

        human = _human_size(size) if size is not None else "unknown"
        checksum = _file_checksum(outfile) if (size is not None and size < (1 << 30)) else ""

        mode = getattr(config.run, "summary_mode", "user") or "user"
        compact = bool(getattr(config.run, "compact", False))
        summary_json = bool(getattr(config.run, "summary_json", False))
        summary_json_path = getattr(config.run, "summary_json_path", None)

        success = bool(size is not None and size > 0 and Path(outfile).exists())

        # Compact single-line mode
        if compact:
            compact_line = (
                f"Run {getattr(config.run, 'run_number', 'unknown')} "
                f"{'✔' if success else '❌'} | {events_written} events"
                f" | {total_time:.2f} s | {throughput:.2f} ev/s"
                f" | output: {Path(str(outfile)).name if outfile else 'None'}"
            )
            if mode == "user":
                print(compact_line)
            else:
                logger.info(compact_line)
        else:
            # USER-friendly multi-line narrative
            header = "🚀 Prometheus Simulation"
            header_lines = []
            header_lines.append(header)
            header_lines.append("")
            header_lines.append(f"Run ID:        {getattr(config.run, 'run_number', 'unknown')}")
            header_lines.append(f"Events:        {getattr(config.run, 'nevents', 'unknown')}")
            header_lines.append(f"Injector:      {getattr(config.injection, 'name', 'unknown')}")
            header_lines.append(
                f"Propagator:    {getattr(config.photon_propagator, 'name', 'unknown')}"
            )
            modules = getattr(
                prom.detector, "n_modules", len(getattr(prom.detector, "modules", []))
            )
            header_lines.append(f"Modules:       {modules}")
            header_lines.append("")
            header_lines.append("─" * 64)

            # Phase 1 - Injection
            phase_lines = []
            phase_lines.append("")
            phase_lines.append("[1/3] Injection")
            if hasattr(prom, "_start_inj") and hasattr(prom, "_end_inj"):
                phase_lines.append(f"✔ Completed in {inj_time:.2f} s")
                phase_lines.append(f"  → Events generated: {events_written}")
            else:
                phase_lines.append("✖ Not completed")

            # Phase 2 - Propagation
            phase_lines.append("")
            phase_lines.append("[2/3] Propagation")
            if hasattr(prom, "_start_prop") and hasattr(prom, "_end_prop"):
                phase_lines.append(f"✔ Completed in {prop_time:.2f} s")
                try:
                    parts = set()
                    for ev in prom.injection:
                        for fs in getattr(ev, "final_states", []):
                            parts.add(str(fs))
                    parts_list = ", ".join(list(parts)[:5])
                    if parts_list:
                        phase_lines.append(f"  → Particles tracked: {parts_list}")
                except Exception:
                    pass
            else:
                phase_lines.append("✖ Not completed")

            # Phase 3 - Output
            phase_lines.append("")
            phase_lines.append("[3/3] Output")
            if success:
                phase_lines.append("✔ File written")
                phase_lines.append(f"  → {outfile} ({human})")
            else:
                phase_lines.append("✖ No output written")

            footer_lines = []
            footer_lines.append("")
            footer_lines.append("─" * 64)
            footer_lines.append("")
            footer_lines.append(
                "✅ Simulation completed successfully" if success else "❌ Simulation failed"
            )
            footer_lines.append("")
            footer_lines.append("Summary:")
            footer_lines.append(f"  Total time:     {total_time:.2f} s")
            footer_lines.append(f"  Throughput:     {throughput:.2f} events/s")
            footer_lines.append(f"  Output events:  {events_written}")
            footer_lines.append("")

            # Emit user view
            user_view = "\n".join(header_lines + phase_lines + footer_lines)
            if mode == "user":
                print(user_view)
            else:
                logger.info(user_view)

            # Prominent output pointer
            try:
                output_pointer = f"📦 Output ready:\n   {outfile}\n"
                if mode == "user":
                    print(output_pointer)
                else:
                    logger.info(output_pointer)
            except Exception:
                pass

            # Collate captured warnings and internal prints
            captured_warnings = getattr(prom, "_captured_warnings", []) or []
            init_noise = bool(
                getattr(prom, "_init_output", None) and getattr(prom, "_init_output", "").strip()
            )
            inject_noise = bool(
                getattr(prom, "_inject_output", None)
                and getattr(prom, "_inject_output", "").strip()
            )
            prop_noise = bool(
                getattr(prom, "_propagate_output", None)
                and getattr(prom, "_propagate_output", "").strip()
            )
            internal_noise_count = int(init_noise) + int(inject_noise) + int(prop_noise)
            total_warnings = len(captured_warnings) + (warn + err + crit)

            if total_warnings + internal_noise_count > 0:
                if mode == "user":
                    print(f"⚠ Warnings detected ({total_warnings + internal_noise_count})")
                    print(
                        "  Hint: set config.run.summary_mode='debug' "
                        "(and optionally config.run.verbosity='DEBUG') to inspect details"
                    )
                else:
                    logger.warning(
                        "⚠ Warnings detected (%d)", total_warnings + internal_noise_count
                    )
                    for w in captured_warnings:
                        logger.warning("  - %s", w)
                    if init_noise:
                        logger.debug("--- Init output (excerpt) ---")
                        for line_ in getattr(prom, "_init_output", "").splitlines()[:200]:
                            logger.debug(line_)
                    if inject_noise:
                        logger.debug("--- Injection output (excerpt) ---")
                        for line_ in getattr(prom, "_inject_output", "").splitlines()[:200]:
                            logger.debug(line_)
                    if prop_noise:
                        logger.debug("--- Propagation output (excerpt) ---")
                        for line_ in getattr(prom, "_propagate_output", "").splitlines()[:200]:
                            logger.debug(line_)

        # Debug/details mode: emit the richer, developer-oriented summary at DEBUG
        if mode.lower() == "debug":
            debug_lines = [
                (
                    f"Run {getattr(config.run, 'run_number', 'unknown')}"
                    f" | requested_nevents={getattr(config.run, 'nevents', 'unknown')}"
                    f" | events_written={events_written}"
                ),
                f"Timings [s]: inj={inj_time:.3f} prop={prop_time:.3f}"
                f" write={write_time:.3f} total={total_time:.3f}",
                f"Throughput: {throughput:.2f} ev/s",
                f"Start: {start_ts} | End: {end_ts}",
                f"Output: {outfile} ({human})",
                f"Checksum (sha256): {checksum}",
                f"Logs: {logfile}",
                f"External subprocess statuses: {external_statuses}",
                f"Warnings: {warn} Errors: {err} Critical: {crit}",
            ]
            logger.debug("Run debug summary:\n%s", "\n".join(debug_lines))

        # Optionally write a JSON summary for automation
        if summary_json and outfile:
            try:
                jpath = summary_json_path or (outfile + ".summary.json")
                jdata = {
                    "run_number": getattr(config.run, "run_number", None),
                    "requested_nevents": getattr(config.run, "nevents", None),
                    "events_written": events_written,
                    "timings": {
                        "inj": inj_time,
                        "prop": prop_time,
                        "write": write_time,
                        "total": total_time,
                    },
                    "throughput_ev_s": throughput,
                    "output": {"path": outfile, "size": size, "checksum": checksum},
                    "logs": logfile,
                    "external_statuses": external_statuses,
                    "warnings": {"warning": warn, "error": err, "critical": crit},
                    "start": start_ts,
                    "end": end_ts,
                    "success": success,
                }
                with open(jpath, "w") as jf:
                    json.dump(jdata, jf, indent=2)
                logger.info("Wrote run JSON summary to %s", jpath)
            except Exception:
                logger.debug("Failed to write JSON summary to %s", jpath)

    except Exception:
        logger.debug("Failed to produce run summary")