Source code for arcade.hardware.report_parser

"""Parse Vivado synthesis reports for utilization and timing."""

from __future__ import annotations

import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, Union

if TYPE_CHECKING:
    from .estimator import ResourceReport

from .._logging import get_logger

log = get_logger(__name__)


[docs] def parse_synth_report( project_dir: Union[str, Path], *, report_file: str | None = None, ) -> dict[str, Any]: """Parse a Vivado synthesis report and return key metrics. Args: project_dir: Path to the HLS4ML / Vivado project directory. report_file: Specific report file to parse. If ``None``, auto-detects by searching for common filenames. Returns: Dict with ``"lut"``, ``"ff"``, ``"bram"``, ``"dsp"``, ``"latency_cycles"``, ``"clock_period_ns"``, ``"latency_ns"``, and ``"report_path"``. Raises: FileNotFoundError: If no report file is found. """ project_dir = Path(project_dir) if report_file: rpt = project_dir / report_file else: rpt = _find_report(project_dir) if rpt is None or not rpt.exists(): raise FileNotFoundError( f"No synthesis report found in {project_dir}. Run Vivado synthesis first." ) text = rpt.read_text() result: dict[str, Any] = { "report_path": str(rpt), "lut": _extract_resource(text, "LUT"), "ff": _extract_resource(text, "FF"), "bram": _extract_resource(text, "BRAM"), "dsp": _extract_resource(text, "DSP"), "latency_cycles": _extract_latency(text), "clock_period_ns": _extract_clock(text), } if result["latency_cycles"] is not None and result["clock_period_ns"] is not None: result["latency_ns"] = result["latency_cycles"] * result["clock_period_ns"] else: result["latency_ns"] = None log.info( "Parsed report %s: LUT=%s, FF=%s, DSP=%s, latency=%s cycles", rpt.name, result["lut"], result["ff"], result["dsp"], result["latency_cycles"], ) return result
def _find_report(project_dir: Path) -> Path | None: """Search common locations for Vivado HLS report files.""" candidates = [ *project_dir.glob("**/*_csynth.rpt"), *project_dir.glob("**/vivado_synth.rpt"), *project_dir.glob("**/*_utilization_synth.rpt"), *project_dir.glob("**/*.rpt"), ] if candidates: return sorted(candidates, key=lambda p: p.stat().st_mtime, reverse=True)[0] return None def _extract_resource(text: str, resource: str) -> int | None: """Extract a resource count from a Vivado utilization table.""" patterns = [ rf"\|\s*{resource}\s*\|\s*(\d+)", rf"{resource}\s*[:=]\s*(\d+)", rf"Total\s+{resource}s?\s*[:=]\s*(\d+)", ] for pat in patterns: m = re.search(pat, text, re.IGNORECASE) if m: return int(m.group(1)) return None def _extract_latency(text: str) -> int | None: patterns = [ r"Latency\s*\(cycles\)\s*[:=]\s*(\d+)", r"\|\s*Latency\s*\|\s*(\d+)", r"worst\s+case\s+latency\s*[:=]\s*(\d+)", ] for pat in patterns: m = re.search(pat, text, re.IGNORECASE) if m: return int(m.group(1)) return None def _extract_clock(text: str) -> float | None: patterns = [ r"Clock\s*Period\s*\(ns\)\s*[:=]\s*([\d.]+)", r"Target\s+Clock\s*[:=]\s*([\d.]+)\s*ns", r"ap_clk\s*\|\s*([\d.]+)", ] for pat in patterns: m = re.search(pat, text, re.IGNORECASE) if m: return float(m.group(1)) return None def parse_quartus_report(path: Union[str, Path]) -> "ResourceReport": """Parse a Quartus synthesis report and return a :class:`ResourceReport`. Args: path: Path to the Quartus ``.fit.rpt`` or ``.map.rpt`` file. Returns: A :class:`ResourceReport` populated from synthesis results. Raises: FileNotFoundError: If the report file does not exist. NotImplementedError: Always (stub implementation). """ path = Path(path) if not path.exists(): raise FileNotFoundError(f"Quartus report not found: {path}") # TODO: Implement Quartus report parsing. # Quartus reports use different table formats and resource names # (ALMs instead of LUTs, ALUTs, M20K instead of BRAM, etc.). raise NotImplementedError( "Quartus report parsing is not yet implemented. " "Contributions welcome — see the Vivado parser above for reference." )