Files
PlantAI-rag-new/bioflow_kernel/scripts/render_extension_publication_figures.py
T

343 lines
18 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
from __future__ import annotations
import csv
import html
import json
from pathlib import Path
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch, Rectangle
import numpy as np
PROJECT_ROOT = Path(__file__).resolve().parents[1]
ROOT = PROJECT_ROOT / "项目记录" / "拓展分析阶段_发表级完整实验_20260616"
FIG_DIR = ROOT / "figures" / "nature_style"
TABLE_DIR = ROOT / "tables" / "nature_style"
REPORT = ROOT / "summary_report_publication.html"
mpl.rcParams.update({
"font.family": "sans-serif",
"font.sans-serif": ["Arial", "Helvetica", "DejaVu Sans", "sans-serif"],
"svg.fonttype": "none",
"pdf.fonttype": 42,
"font.size": 7,
"axes.spines.right": False,
"axes.spines.top": False,
"axes.linewidth": 0.7,
"legend.frameon": False,
})
PALETTE = {
"ink": "#1b2430",
"muted": "#687386",
"line": "#d6dce6",
"green": "#2f7d55",
"green_soft": "#dcefe4",
"red": "#b33a3a",
"red_soft": "#f3d7d7",
"blue": "#2d6f9f",
"blue_soft": "#dce9f5",
"amber": "#b5791f",
"amber_soft": "#f4e3c5",
"grey": "#f2f4f7",
}
def load_json(rel: str):
return json.loads((ROOT / rel).read_text(encoding="utf-8"))
def short_name(name: str) -> str:
return {
"sequence_extraction": "Sequence\nextraction",
"hmmer_domain": "HMMER\ndomain",
"motif_meme": "MEME\nmotif",
"phylogeny_tree": "Phylogeny\ntree",
"reference_classification": "Reference\nclassification",
"synteny_collinearity": "Synteny\ncollinearity",
"kaks_pairwise": "Ka/Ks\npairwise",
"pep_properties": "Protein\nproperties",
}.get(name, name.replace("_", "\n"))
def save_all(fig, stem: Path):
stem.parent.mkdir(parents=True, exist_ok=True)
fig.savefig(stem.with_suffix(".svg"), bbox_inches="tight")
fig.savefig(stem.with_suffix(".pdf"), bbox_inches="tight")
fig.savefig(stem.with_suffix(".png"), dpi=450, bbox_inches="tight")
fig.savefig(stem.with_suffix(".tiff"), dpi=600, bbox_inches="tight")
def panel_label(ax, label):
ax.text(-0.04, 1.04, label, transform=ax.transAxes, fontweight="bold", fontsize=10, va="top", ha="left", color=PALETTE["ink"])
def draw_workflow(ax):
ax.set_axis_off()
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
steps = [
("Executable\ncode + data", PALETTE["blue_soft"], 0.05),
("Spec\nconversion", "#ffffff", 0.23),
("Deterministic\nwrapper", PALETTE["green_soft"], 0.41),
("Golden\ncomparison", PALETTE["amber_soft"], 0.59),
("Template +\npublish gates", PALETTE["amber_soft"], 0.77),
("Published\nextension", PALETTE["green_soft"], 0.91),
]
y = 0.62
for i, (txt, face, x) in enumerate(steps):
w = 0.13 if i < 5 else 0.11
patch = FancyBboxPatch((x - w/2, y - 0.105), w, 0.21, boxstyle="round,pad=0.012,rounding_size=0.012", fc=face, ec=PALETTE["line"], lw=0.9)
ax.add_patch(patch)
ax.text(x, y, txt, ha="center", va="center", fontsize=7.2, color=PALETTE["ink"], fontweight="bold" if i in {2, 3, 4, 5} else "normal")
if i < len(steps) - 1:
x2 = steps[i + 1][2]
ax.add_patch(FancyArrowPatch((x + w/2 + 0.005, y), (x2 - 0.065, y), arrowstyle="-|>", mutation_scale=8, lw=0.9, color=PALETTE["muted"]))
ax.text(0.05, 0.88, "Validation-driven extension-stage construction", fontsize=10, fontweight="bold", color=PALETTE["ink"], ha="left")
ax.text(0.05, 0.78, "API output is treated as a candidate; publication requires deterministic execution evidence.", fontsize=7.5, color=PALETTE["muted"], ha="left")
ax.add_patch(FancyBboxPatch((0.53, 0.18), 0.36, 0.18, boxstyle="round,pad=0.012,rounding_size=0.012", fc=PALETTE["red_soft"], ec="#d8a5a5", lw=0.8))
ax.text(0.71, 0.27, "Failed candidates are blocked\nbefore entering the published stage list", ha="center", va="center", fontsize=7.5, color=PALETTE["ink"])
ax.add_patch(FancyArrowPatch((0.77, 0.52), (0.72, 0.36), arrowstyle="-|>", mutation_scale=8, lw=0.9, color=PALETTE["red"]))
def draw_lifecycle_matrix(ax, det_rows, api_rows):
phases = ["Build", "Trial", "Golden", "Template\ngate", "Publish"]
cases = [r["archetype_id"] for r in det_rows]
values = []
for case in cases:
d = next(r for r in det_rows if r["archetype_id"] == case)
a = next((r for r in api_rows if r["archetype_id"] == case), {})
values.append([bool(d["build_success"]), bool(d["trial_success"]), d["golden_status"] == "passed", d["template_gate_status"] == "passed", bool(d["publish_success"])])
values.append([bool(a.get("build_success")), bool(a.get("trial_success")), a.get("golden_status") == "passed", a.get("template_gate_status") == "passed", bool(a.get("publish_success"))])
ax.set_xlim(0, len(phases))
ax.set_ylim(0, len(values))
ax.invert_yaxis()
for y, row in enumerate(values):
for x, val in enumerate(row):
ax.add_patch(Rectangle((x, y), 1, 1, fc=PALETTE["green_soft"] if val else PALETTE["red_soft"], ec="white", lw=1.2))
ax.text(x + 0.5, y + 0.5, "✓" if val else "×", ha="center", va="center", fontsize=8, color=PALETTE["green"] if val else PALETTE["red"], fontweight="bold")
for y in range(0, len(values), 2):
ax.add_patch(Rectangle((0, y), len(phases), 2, fill=False, ec=PALETTE["line"], lw=0.6))
ax.set_xticks(np.arange(len(phases)) + 0.5, phases, fontsize=7)
ylabels = []
for case in cases:
ylabels.extend([short_name(case) + "\nD", short_name(case) + "\nAPI"])
ax.set_yticks(np.arange(len(values)) + 0.5, ylabels, fontsize=5.8)
ax.tick_params(length=0)
for sp in ax.spines.values():
sp.set_visible(False)
ax.set_title("Lifecycle gate outcomes for deterministic and API/Agent conversion", loc="left", fontsize=8.5, fontweight="bold", pad=8)
def draw_api_attempts(ax, api_rows):
names = [short_name(r["archetype_id"]) for r in api_rows]
attempts = [int(r.get("api_attempt_count") or 0) for r in api_rows]
ok = [bool(r.get("publish_success")) for r in api_rows]
colors = [PALETTE["blue"] if x else PALETTE["red"] for x in ok]
y = np.arange(len(names))
ax.barh(y, attempts, color=colors, height=0.62)
for yi, val, success in zip(y, attempts, ok):
ax.text(val + 0.2, yi, str(val), va="center", ha="left", fontsize=7, color=PALETTE["ink"])
if not success:
ax.text(max(val * 0.45, 0.2), yi, "blocked", va="center", ha="center", fontsize=6.5, color="white", fontweight="bold")
ax.set_yticks(y, names, fontsize=6.6)
ax.invert_yaxis()
ax.set_xlabel("API attempts before final decision", fontsize=7)
ax.set_xlim(0, max(attempts) + 2)
ax.grid(axis="x", color=PALETTE["line"], lw=0.5)
ax.set_title("API/Agent conversion required variable repair effort", loc="left", fontsize=8.5, fontweight="bold", pad=8)
def draw_summary_bars(ax, det, api, neg):
labels = ["Deterministic\nchain", "API/Agent\nconversion", "Negative\ncontrols"]
numerator = [det["passed"], api["passed"], neg["blocked"]]
denom = [det["case_count"], api["case_count"], neg["case_count"]]
colors = [PALETTE["green"], PALETTE["blue"], PALETTE["amber"]]
x = np.arange(3)
ax.bar(x, [n / d for n, d in zip(numerator, denom)], color=colors, width=0.58)
for xi, n, d in zip(x, numerator, denom):
ax.text(xi, n / d + 0.045, f"{n}/{d}", ha="center", va="bottom", fontsize=8, fontweight="bold", color=PALETTE["ink"])
ax.set_xticks(x, labels, fontsize=7)
ax.set_ylim(0, 1.16)
ax.set_ylabel("Fraction passing expected decision", fontsize=7)
ax.set_yticks([0, 0.5, 1.0], ["0", "0.5", "1.0"])
ax.grid(axis="y", color=PALETTE["line"], lw=0.5)
ax.set_title("Three independent evidence layers support safe extensibility", loc="left", fontsize=8.5, fontweight="bold", pad=8)
def draw_negative_controls(ax, rows):
labels = [r["scenario"].replace("_", "\n") for r in rows]
y = np.arange(len(rows))
ax.barh(y, [1] * len(rows), color=PALETTE["red_soft"], height=0.6, edgecolor="#d8a5a5", lw=0.6)
for yi, r in zip(y, rows):
ax.text(0.5, yi, "blocked" if r["blocked"] and not r["published"] else "leaked", ha="center", va="center", fontsize=7, color=PALETTE["red"], fontweight="bold")
ax.set_yticks(y, labels, fontsize=6.7)
ax.set_xticks([])
ax.set_xlim(0, 1)
ax.invert_yaxis()
for sp in ax.spines.values():
sp.set_visible(False)
ax.set_title("Failure-injection cases were blocked before publication", loc="left", fontsize=8.5, fontweight="bold", pad=8)
def write_source_tables(det, api, neg):
TABLE_DIR.mkdir(parents=True, exist_ok=True)
with (TABLE_DIR / "source_data_main_figure.csv").open("w", encoding="utf-8", newline="") as f:
fields = ["group", "archetype_id", "build", "trial", "golden", "template_gate", "publish", "api_attempts", "error"]
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
for group, rows in [("deterministic", det["rows"]), ("api_agent", api["rows"] )]:
for r in rows:
w.writerow({
"group": group,
"archetype_id": r["archetype_id"],
"build": r.get("build_success"),
"trial": r.get("trial_success"),
"golden": r.get("golden_status"),
"template_gate": r.get("template_gate_status"),
"publish": r.get("publish_success"),
"api_attempts": r.get("api_attempt_count", ""),
"error": r.get("error", ""),
})
with (TABLE_DIR / "source_data_negative_controls.csv").open("w", encoding="utf-8", newline="") as f:
fields = ["scenario", "blocked", "published", "error"]
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
for r in neg["rows"]:
w.writerow({k: r.get(k, "") for k in fields})
def make_main_figure(det, api, neg):
fig = plt.figure(figsize=(7.2, 8.0), constrained_layout=False)
gs = fig.add_gridspec(3, 2, height_ratios=[1.02, 1.6, 1.0], width_ratios=[1.18, 0.82], hspace=0.52, wspace=0.36)
axA = fig.add_subplot(gs[0, :])
axB = fig.add_subplot(gs[1, 0])
axC = fig.add_subplot(gs[1, 1])
axD = fig.add_subplot(gs[2, 0])
axE = fig.add_subplot(gs[2, 1])
draw_workflow(axA)
panel_label(axA, "a")
draw_lifecycle_matrix(axB, det["rows"], api["rows"])
panel_label(axB, "b")
draw_api_attempts(axC, api["rows"])
panel_label(axC, "c")
draw_summary_bars(axD, det, api, neg)
panel_label(axD, "d")
draw_negative_controls(axE, neg["rows"])
panel_label(axE, "e")
fig.text(0.012, 0.012, "Source data: real extension-stage publication experiment package; deterministic chain n=8, API/Agent n=8, negative controls n=4.", fontsize=6.5, color=PALETTE["muted"])
save_all(fig, FIG_DIR / "figure_1_extension_stage_validation")
plt.close(fig)
def make_html(det, api, neg):
failed = [r for r in api["rows"] if not r.get("publish_success")]
failure_text = failed[0]["error"] if failed else ""
rows_det = "".join(
f"<tr><td>{html.escape(r['archetype_id'])}</td><td>{r['build_success']}</td><td>{r['trial_success']}</td><td>{html.escape(str(r['golden_status']))}</td><td>{html.escape(str(r['template_gate_status']))}</td><td>{r['publish_success']}</td><td>{r.get('output_count')}</td></tr>"
for r in det["rows"]
)
rows_api = "".join(
f"<tr><td>{html.escape(r['archetype_id'])}</td><td>{r.get('api_attempt_count')}</td><td>{html.escape(str(r.get('golden_status')))}</td><td>{html.escape(str(r.get('template_gate_status')))}</td><td>{r.get('publish_success')}</td><td>{html.escape(str(r.get('error') or '')[:220])}</td></tr>"
for r in api["rows"]
)
rows_neg = "".join(
f"<tr><td>{html.escape(r['scenario'])}</td><td>{r['blocked']}</td><td>{r['published']}</td><td>{html.escape(r['error'][:220])}</td></tr>"
for r in neg["rows"]
)
doc = f"""<!doctype html>
<html lang=\"zh-CN\">
<head>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<title>拓展分析阶段发表级实验结果</title>
<style>
:root {{ --ink:#1b2430; --muted:#687386; --line:#d6dce6; --blue:#2d6f9f; --green:#2f7d55; --red:#b33a3a; --amber:#b5791f; --panel:#f7f9fc; }}
body {{ margin:0; font:14px/1.68 Arial, 'Microsoft YaHei', sans-serif; color:var(--ink); background:#fff; }}
main {{ max-width:1180px; margin:0 auto; padding:30px 28px 60px; }}
h1 {{ font-size:28px; margin:0 0 8px; }}
h2 {{ margin:34px 0 12px; font-size:20px; border-left:5px solid var(--blue); padding-left:10px; }}
h3 {{ margin:20px 0 8px; font-size:16px; }}
.subtitle {{ color:var(--muted); max-width:920px; }}
.grid {{ display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; margin:18px 0; }}
.metric {{ border:1px solid var(--line); background:var(--panel); padding:14px; border-radius:8px; }}
.metric b {{ display:block; font-size:24px; color:var(--blue); }}
.metric span {{ color:var(--muted); }}
.note {{ border-left:4px solid var(--blue); background:#eaf2fa; padding:11px 13px; margin:12px 0; }}
.warn {{ border-left-color:var(--amber); background:#f7ecd7; }}
.good {{ color:var(--green); font-weight:700; }}
.bad {{ color:var(--red); font-weight:700; }}
table {{ width:100%; border-collapse:collapse; margin:12px 0 22px; table-layout:fixed; }}
th,td {{ border:1px solid var(--line); padding:8px 9px; vertical-align:top; word-break:break-word; }}
th {{ background:#edf2f8; text-align:left; }}
.figure {{ border:1px solid var(--line); padding:12px; border-radius:8px; background:#fff; margin:14px 0 22px; }}
.figure img {{ max-width:100%; display:block; margin:auto; }}
.caption {{ color:var(--muted); font-size:13px; margin-top:8px; }}
code {{ background:#f2f4f7; padding:1px 4px; border-radius:4px; font-size:12px; }}
@media (max-width:850px) {{ .grid {{ grid-template-columns:1fr 1fr; }} main {{ padding:20px 14px 42px; }} table {{ font-size:12px; }} }}
</style>
</head>
<body><main>
<header>
<h1>拓展分析阶段:发表级实验结果</h1>
<p class=\"subtitle\">本文档将前期真实执行结果整理为论文结果写作状态。核心结论是:确定性拓展阶段构建链路在 8 类真实数据 archetype 上全部通过;API/Agent 自动转写在同一任务集上达到 7/8 发布成功,失败案例被 golden validation 与发布门禁拦截。</p>
</header>
<section>
<h2>一、结果总览</h2>
<div class=\"grid\">
<div class=\"metric\"><b>{det['passed']}/{det['case_count']}</b><span>确定性构建链路通过</span></div>
<div class=\"metric\"><b>{api['passed']}/{api['case_count']}</b><span>API/Agent 转写发布成功</span></div>
<div class=\"metric\"><b>{neg['blocked']}/{neg['case_count']}</b><span>结构性负例被阻断</span></div>
<div class=\"metric\"><b>60</b><span>拓展阶段相关单元测试通过</span></div>
</div>
<div class=\"note\">论文表述应分开写:<b>确定性主链路 8/8</b> 证明系统闭环稳定;<b>API/Agent 7/8</b> 是自动转写能力对照;<b>负例 4/4 blocked</b> 证明发布门禁有效。</div>
</section>
<section>
<h2>二、主图</h2>
<div class=\"figure\"><img src=\"figures/nature_style/figure_1_extension_stage_validation.svg\" alt=\"Extension-stage validation multi-panel figure\"><div class=\"caption\"><b>Figure 1 | Validation-driven construction of extension analysis stages.</b> a, Extension-stage construction and publication workflow. b, Lifecycle gate outcomes for deterministic and API/Agent conversion across eight archetypes. D = deterministic chain; API = DeepSeek/API Agent conversion. c, API attempts required before final decision. d, Summary of three evidence layers. e, Negative-control cases blocked before publication.</div></div>
<p>导出文件:<code>figure_1_extension_stage_validation.svg</code>, <code>.pdf</code>, <code>.png</code>, <code>.tiff</code>。Source data 已导出到 <code>tables/nature_style/</code>。</p>
</section>
<section>
<h2>三、确定性主链路</h2>
<p>8 类真实数据 archetype 均通过 build、trial、golden comparison、template gate 和 publish gate。该结果可作为论文主结果。</p>
<table><thead><tr><th>Archetype</th><th>Build</th><th>Trial</th><th>Golden</th><th>Template gate</th><th>Publish</th><th>Outputs</th></tr></thead><tbody>{rows_det}</tbody></table>
</section>
<section>
<h2>四、API/Agent 对照</h2>
<p>API/Agent 对照完整覆盖同一 8 类任务,结果为 <b>{api['passed']}/{api['case_count']}</b> 发布成功。失败的 <code>hmmer_domain</code> 并非系统执行链路失败,而是 API 生成的 semantic command template 未复现原始行为,被 golden/publish gate 阻断。</p>
<div class=\"note warn\">失败原因摘要:{html.escape(failure_text[:420])}</div>
<table><thead><tr><th>Archetype</th><th>API attempts</th><th>Golden</th><th>Template gate</th><th>Publish</th><th>Error</th></tr></thead><tbody>{rows_api}</tbody></table>
</section>
<section>
<h2>五、失败拦截负例</h2>
<p>4 个结构性负例全部被阻断,没有进入 published stage 列表。该实验应称为 <b>negative-control / failure-injection test</b>,不是严格意义的消融实验。</p>
<table><thead><tr><th>Scenario</th><th>Blocked</th><th>Published</th><th>Blocking evidence</th></tr></thead><tbody>{rows_neg}</tbody></table>
</section>
<section>
<h2>六、论文可用表述</h2>
<p>推荐写法:</p>
<div class=\"note\">We evaluated the extension-stage construction framework using eight real-data executable-code archetypes. The deterministic construction pipeline passed all eight cases through trial execution, original-vs-converted golden comparison, template-gate validation and publish-gate checking. In an API/Agent conversion benchmark on the same cases, seven of eight stages were successfully published, while one HMMER-domain case was blocked because the API-generated semantic command template failed to reproduce the original behavior. Four structural negative controls were all intercepted before publication.</div>
<p>不应写成:<span class=\"bad\">API/LLM 自动转写 8/8 全部成功</span>。</p>
</section>
</main></body></html>
"""
REPORT.write_text(doc, encoding="utf-8")
def main():
FIG_DIR.mkdir(parents=True, exist_ok=True)
TABLE_DIR.mkdir(parents=True, exist_ok=True)
det = load_json("results/archetype_spec/summary.json")
api = load_json("results/deepseek_agent/summary.json")
neg = load_json("negative_cases/negative_control_summary.json")
write_source_tables(det, api, neg)
make_main_figure(det, api, neg)
make_html(det, api, neg)
print(REPORT)
if __name__ == "__main__":
main()