fix(SAC_LSTM_Bot): atomic+validated SVG output for progress graphs
This commit is contained in:
@@ -15,9 +15,11 @@ All args optional; defaults relative to the SAC_LSTM_Bot/ root (parent of tools/
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
@@ -80,6 +82,22 @@ def esc(s):
|
||||
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def write_svg(path, text):
|
||||
"""Validate the finished SVG, then atomically swap it into place.
|
||||
|
||||
Readers never see partial output; an invalid render aborts without
|
||||
touching the previous good file."""
|
||||
try:
|
||||
ET.fromstring(text)
|
||||
except ET.ParseError as e:
|
||||
print(f"[error] {path.name}: generated SVG invalid, keeping old file ({e})")
|
||||
return False
|
||||
tmp = path.with_name(path.name + ".tmp")
|
||||
tmp.write_text(text)
|
||||
os.replace(tmp, path)
|
||||
return True
|
||||
|
||||
|
||||
def svg_open(w, h, title):
|
||||
return (f'<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="{h}" '
|
||||
f'viewBox="0 0 {w} {h}" font-family="sans-serif">\n'
|
||||
@@ -238,8 +256,8 @@ def graph_eval(series_v2, series_v1, out):
|
||||
items_b.append((COLORS["Corners"], f"Run 3 trend (rolling-{TREND_WINDOW})"))
|
||||
s += legend(items_b, x0 + 12, y1b + 14)
|
||||
|
||||
out.write_text(s)
|
||||
return 1
|
||||
s += "</svg>\n"
|
||||
return 1 if write_svg(out, s) else 0
|
||||
|
||||
|
||||
# ---------- graph 2: loss curves ----------
|
||||
@@ -319,8 +337,7 @@ def graph_losses(rows, out):
|
||||
fixed_range=(0, max(1.0, max(alpha))))
|
||||
s += (f'<text x="{x0 + (x1 - x0) // 2}" y="{H - 8}" text-anchor="middle" '
|
||||
f'font-size="12">metric line number ({n} rows)</text>\n</svg>\n')
|
||||
out.write_text(s)
|
||||
return 1
|
||||
return 1 if write_svg(out, s) else 0
|
||||
|
||||
|
||||
# ---------- selftest ----------
|
||||
@@ -351,6 +368,8 @@ def selftest():
|
||||
ok = graph_losses(rows, td / "g2.svg") and graph_eval(ser, {"Corners": [0, 10]}, td / "g1.svg")
|
||||
assert ok and (td / "g1.svg").stat().st_size > 500
|
||||
g1 = (td / "g1.svg").read_text()
|
||||
ET.fromstring(g1) # whole doc must parse -> closing tag present
|
||||
ET.fromstring((td / "g2.svg").read_text())
|
||||
assert 'width="1200"' in g1, "canvas must be >=1200 wide"
|
||||
assert g1.count("<circle") == sum(len(v) for v in ser.values()) # one dot per raw eval
|
||||
n_trend = sum(1 for v in ser.values() if len(v) >= 2) + 2 # trends + v1 buckets + v3 trend
|
||||
@@ -377,11 +396,17 @@ def main():
|
||||
series_v1 = parse_eval_series(v1log)
|
||||
print(f"[info] v1 evals parsed: " +
|
||||
", ".join(f"{k}={len(v)}" for k, v in sorted(series_v1.items())) or "(none)")
|
||||
made += graph_eval(series_v2, series_v1, outdir / "graph_eval_winrates.svg")
|
||||
try:
|
||||
made += graph_eval(series_v2, series_v1, outdir / "graph_eval_winrates.svg")
|
||||
except Exception as e:
|
||||
print(f"[error] win-rate graph failed (loss graph still attempted): {e}")
|
||||
|
||||
rows = parse_metrics(metrics)
|
||||
print(f"[info] metric rows parsed: {len(rows)}")
|
||||
made += graph_losses(rows, outdir / "graph_losses.svg")
|
||||
try:
|
||||
made += graph_losses(rows, outdir / "graph_losses.svg")
|
||||
except Exception as e:
|
||||
print(f"[error] loss graph failed: {e}")
|
||||
|
||||
if made:
|
||||
print(f"[done] {made} graph(s) written to {outdir}")
|
||||
|
||||
Reference in New Issue
Block a user