"""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."
)