#!/usr/bin/env python3 """Refresh report_metrics.tex from versioned Vivado and test evidence.""" from __future__ import annotations import datetime as dt import re import subprocess from pathlib import Path REPORT_DIR = Path(__file__).resolve().parent.parent ROOT = REPORT_DIR.parents[2] METRICS = REPORT_DIR / "report_metrics.tex" def read_first(*paths: Path) -> str: for path in paths: if path.is_file(): return path.read_text(encoding="utf-8", errors="replace") return "" def set_macro(text: str, name: str, value: str) -> str: pattern = re.compile(r"(\\newcommand\{\\" + re.escape(name) + r"\}\{)[^\n}]*(\})") if not pattern.search(text): raise ValueError(f"macro not found: {name}") return pattern.sub(lambda m: m.group(1) + value + m.group(2), text, count=1) def number(value: str) -> str: raw = value.replace(",", "").strip() if re.fullmatch(r"-?\d+", raw): return f"{int(raw):,}" return raw def capture(pattern: str, text: str, group: int = 1) -> str | None: match = re.search(pattern, text, re.MULTILINE) return match.group(group).strip() if match else None def update_timing(metrics: str, timing: str) -> str: summary = capture(r"^\s*(-?\d+\.\d+)\s+-?\d+\.\d+\s+\d+\s+\d+\s+", timing) if summary: metrics = set_macro(metrics, "OverallWNS", summary) for clock, prefix in (("cpu_clk", "Cpu"), ("sys_clk", "Sys"), ("mlkem_clk", "Mlkem")): freq = capture(r"^\s*" + clock + r"\s+\{[^\n]+?\}\s+\d+\.\d+\s+(\d+\.\d+)", timing) wns = capture(r"^\s*" + clock + r"\s+(-?\d+\.\d+)\s+", timing) if freq: metrics = set_macro(metrics, prefix + "FreqMHz", freq) if wns: metrics = set_macro(metrics, prefix + "WNS", wns) return metrics def update_util(metrics: str, util: str) -> str: rows = { "SliceLUTs": r"^\| Slice LUTs\s+\|\s*([\d,]+)\s*\|", "SliceFFs": r"^\| Slice Registers\s+\|\s*([\d,]+)\s*\|", "BramTiles": r"^\| Block RAM Tile\s+\|\s*([\d.]+)\s*\|", "DspCount": r"^\| DSPs?\s+\|\s*([\d,]+)\s*\|", } pct = { "SliceLUTPct": r"^\| Slice LUTs\s+\|[^\n]*\|\s*([\d.]+)\s*\|\s*$", "SliceFFPct": r"^\| Slice Registers\s+\|[^\n]*\|\s*([\d.]+)\s*\|\s*$", "BramPct": r"^\| Block RAM Tile\s+\|[^\n]*\|\s*([\d.]+)\s*\|\s*$", } for name, pattern in rows.items(): value = capture(pattern, util) if value: metrics = set_macro(metrics, name, number(value)) for name, pattern in pct.items(): value = capture(pattern, util) if value: metrics = set_macro(metrics, name, value + r"\%") return metrics def update_power(metrics: str, power: str) -> str: mappings = { "TotalPower": r"\| Total On-Chip Power \(W\)\s*\|\s*([\d.]+)", "DynamicPower": r"\| Dynamic \(W\)\s*\|\s*([\d.]+)", "StaticPower": r"\| Device Static \(W\)\s*\|\s*([\d.]+)", } for name, pattern in mappings.items(): value = capture(pattern, power) if value: metrics = set_macro(metrics, name, value) confidence = capture(r"\| Confidence Level\s*\|\s*([^|]+)\|", power) if confidence: metrics = set_macro(metrics, "PowerConfidence", confidence + "(未回标活动率)") return metrics def update_mlkem(metrics: str, log: str) -> str: names = { ("512", "SW"): "MlkemFiveTwelveSW", ("512", "CPU"): "MlkemFiveTwelveCPU", ("512", "DMA"): "MlkemFiveTwelveDMA", ("768", "SW"): "MlkemSevenSixtyEightSW", ("768", "CPU"): "MlkemSevenSixtyEightCPU", ("768", "DMA"): "MlkemSevenSixtyEightDMA", ("1024", "SW"): "MlkemTenTwentyFourSW", ("1024", "CPU"): "MlkemTenTwentyFourCPU", ("1024", "DMA"): "MlkemTenTwentyFourDMA", } for (level, path), name in names.items(): value = capture(rf"^MLKEM{level}_{path}_ONLY_AVG_CYCLES=(\d+)$", log) if value: metrics = set_macro(metrics, name, number(value)) return metrics def update_fft(metrics: str, cpu_log: str, dma_log: str) -> str: cpu_ns = capture(r"Hardware FFT Time:\s*(\d+) ns", cpu_log) dma_ns = capture(r"Hardware FFT Time:\s*(\d+) ns", dma_log) sw_ns = capture(r"Software FFT Time:\s*(\d+) ns", dma_log or cpu_log) cpu_speed = capture(r"Speedup Ratio\s*:\s*([\d.]+)x", cpu_log) dma_speed = capture(r"Speedup Ratio\s*:\s*([\d.]+)x", dma_log) for name, value in (("FftCpuNs", cpu_ns), ("FftDmaNs", dma_ns), ("FftSoftwareNs", sw_ns)): if value: metrics = set_macro(metrics, name, number(value)) for name, value in (("FftCpuSpeedup", cpu_speed), ("FftDmaSpeedup", dma_speed)): if value: metrics = set_macro(metrics, name, value) err_match = re.search(r"Max Abs Error\s*:\s*Re=(\d+),\s*Im=(\d+)", dma_log) if err_match: metrics = set_macro(metrics, "FftMaxError", f"Re={err_match.group(1)},Im={err_match.group(2)}") return metrics def update_matmul(metrics: str, log: str) -> str: elapsed = capture(r"Elapsed time:\s*([\d.]+) ms", log) score = capture(r"Score percent:\s*([\d.]+)", log) if elapsed: metrics = set_macro(metrics, "MatmulBoardMs", elapsed) if score: metrics = set_macro(metrics, "MatmulScore", score + r"\%") if "CRC32 matched." in log: metrics = set_macro(metrics, "MatmulCrcStatus", "PASS") return metrics def main() -> None: metrics = METRICS.read_text(encoding="utf-8") vivado = REPORT_DIR / "evidence" / "vivado" board = REPORT_DIR / "evidence" / "board" legacy = ROOT / "fpga" / "vivado_reports_nodsp" metrics = update_timing(metrics, read_first(vivado / "timing_summary_full.rpt", legacy / "timing_summary_full.rpt")) metrics = update_util(metrics, read_first(vivado / "utilization.rpt", legacy / "utilization.rpt")) metrics = update_power(metrics, read_first(vivado / "power.rpt", legacy / "power.rpt")) soc = REPORT_DIR / "evidence" / "soc" metrics = update_mlkem(metrics, read_first(soc / "mlkem_test.log", ROOT / "sdk/software/examples/mlkem_test/result.txt")) metrics = update_fft( metrics, read_first(board / "fft.log", soc / "fft.log", ROOT / "sdk/software/examples/fft/result.txt"), read_first(board / "fft_dma.log", soc / "fft_dma.log", ROOT / "sdk/software/examples/fft_dma/result.txt"), ) metrics = update_matmul(metrics, read_first(board / "matmul_official.log")) try: revision = subprocess.check_output( ["git", "-C", str(ROOT), "rev-parse", "--short", "HEAD"], text=True ).strip() except (OSError, subprocess.CalledProcessError): revision = "unknown" source = "当前 evidence" if (vivado / "timing_summary_full.rpt").is_file() else "历史报告,待复测" metrics = set_macro(metrics, "EvidenceRevision", f"{revision}({source})") metrics = set_macro(metrics, "EvidenceDate", dt.date.today().isoformat()) METRICS.write_text(metrics, encoding="utf-8") print(f"updated {METRICS}") if __name__ == "__main__": main()