snr_bias / code /scripts /grl_readiness_check.py
cangyeone's picture
Upload GRL reproducibility package
7170296 verified
Raw
History Blame Contribute Delete
8.74 kB
#!/usr/bin/env python3
"""Lightweight GRL manuscript readiness checks for the Overleaf package."""
from __future__ import annotations
import json
import math
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs" / "grl_revision_20260610" / "readiness"
MAIN = ROOT / "main.tex"
FORBIDDEN = [
"TBD",
"to be added",
"placeholder",
"error bars are omitted",
"should be added before submission",
"data DOI: TBD",
"code DOI: TBD",
]
REQUIRED_SECTIONS = [
"Plain Language Summary",
"Keywords",
"Introduction",
"Data and Methods",
"Results",
"Discussion and Conclusions",
"Open Research Section",
"Conflict of Interest declaration",
"AI Tool Disclosure",
]
REQUIRED_FIGURES = [
"figures/snr_matched_design.png",
"figures/fig2_training_combined.pdf",
"figures/fig2_training_combined.png",
"figures/fig3_continuous_association.pdf",
"figures/fig3_continuous_association.png",
]
def strip_latex(text: str) -> str:
text = re.sub(r"(?<!\\)%.*", " ", text)
text = text.replace(r"\%", " percent ")
text = re.sub(r"\\cite[A-Za-z]*\{[^}]*\}", " ", text)
text = re.sub(r"\\[A-Za-z]+\*?(?:\[[^\]]*\])?(?:\{([^{}]*)\})?", r" \1 ", text)
text = re.sub(r"[{}$^_\\]", " ", text)
return text
def word_count(text: str) -> int:
clean = strip_latex(text)
return len(re.findall(r"[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)?", clean))
def extract_env(text: str, env: str) -> str:
match = re.search(rf"\\begin\{{{re.escape(env)}\}}(.*?)\\end\{{{re.escape(env)}\}}", text, re.S)
return match.group(1) if match else ""
def extract_sections_for_pu(text: str) -> str:
parts = []
for env in ["abstract"]:
parts.append(extract_env(text, env))
for match in re.finditer(r"\\(?:section|subsection)\*?\{([^}]*)\}(.*?)(?=\\(?:section|subsection)\*?\{|\\bibliography|\\end\{document\})", text, re.S):
title, body = match.groups()
if title in {"Keywords", "Open Research Section", "Conflict of Interest declaration", "AI Tool Disclosure"}:
parts.append(body)
elif not title.lower().startswith("acknowledg"):
parts.append(body)
captions = re.findall(r"\\caption\{(.*?)\}", text, re.S)
parts.extend(captions)
return "\n".join(parts)
def keypoints(text: str) -> list[str]:
body = extract_env(text, "keypoints")
return [strip_latex(item).strip() for item in re.findall(r"\\item\s+(.*)", body)]
def association_counts_ok(table_text: str) -> bool:
expected = ["576,875", "1,301", "0.556", "1,561", "0.667"]
return all(value in table_text for value in expected)
def main() -> int:
OUT.mkdir(parents=True, exist_ok=True)
failed: list[str] = []
external: list[str] = []
auto_fixable: list[str] = []
files_checked: list[str] = []
if not MAIN.exists():
failed.append("main.tex is missing")
text = ""
else:
text = MAIN.read_text(encoding="utf-8")
files_checked.append(str(MAIN.relative_to(ROOT)))
if not (ROOT / "main.pdf").exists():
auto_fixable.append("main.pdf is missing; run latexmk")
else:
files_checked.append("main.pdf")
if "Requires author confirmation" in text:
external.append("Affiliation and/or corresponding-author email require author confirmation")
if "public repository DOI has not yet been assigned" in text:
external.append("Persistent public data/code repository identifier has not been assigned")
if "funding details require author confirmation" in text:
external.append("Acknowledgments and funding details require author confirmation")
lowered = text.lower()
for token in FORBIDDEN:
if token.lower() in lowered:
failed.append(f"Forbidden manuscript text remains: {token}")
abstract = extract_env(text, "abstract")
abstract_words = word_count(abstract)
if abstract_words > 150:
failed.append(f"Abstract has {abstract_words} words; target is <=150")
kp = keypoints(text)
if len(kp) > 3:
failed.append(f"Key Points count is {len(kp)}; maximum is 3")
for idx, item in enumerate(kp, 1):
if len(item) > 140:
failed.append(f"Key Point {idx} is {len(item)} characters; maximum is 140")
for section in REQUIRED_SECTIONS:
if section not in text:
failed.append(f"Required section is missing: {section}")
for fig in REQUIRED_FIGURES:
path = ROOT / fig
if not path.exists():
failed.append(f"Required figure file is missing: {fig}")
else:
files_checked.append(fig)
table_path = ROOT / "continuous_assoc_table.tex"
if table_path.exists():
table_text = table_path.read_text(encoding="utf-8")
files_checked.append("continuous_assoc_table.tex")
if not association_counts_ok(table_text):
failed.append("Continuous association table does not contain expected retained-pick and event-recall counts")
else:
failed.append("continuous_assoc_table.tex is missing")
if not (ROOT / "si" / "grl_revision" / "supplementary_information.tex").exists():
failed.append("Supporting Information file is missing")
else:
files_checked.append("si/grl_revision/supplementary_information.tex")
bootstrap_candidates = list((ROOT / "outputs").glob("**/*bootstrap*"))
if not bootstrap_candidates:
auto_fixable.append("Bootstrap CI tables are not present; generate after deterministic per-sample outputs exist")
strat_candidates = list((ROOT / "outputs").glob("**/*strat*"))
if not strat_candidates:
auto_fixable.append("SNR-stratified diagnostic tables are not present; generate after deterministic per-sample outputs exist")
pu_words = word_count(extract_sections_for_pu(text))
figures = len(re.findall(r"\\begin\{figure\}", text))
tables = len(re.findall(r"\\begin\{table\}", text)) + len(re.findall(r"\\input\{continuous_assoc_table\}", text))
publication_units = pu_words / 500.0 + figures + tables
if publication_units > 12:
failed.append(f"Publication units are {publication_units:.2f}; maximum is 12")
elif publication_units > 11.5:
auto_fixable.append(f"Publication units are {publication_units:.2f}; internal target is <=11.5")
log_path = ROOT / "main.log"
if log_path.exists():
log_text = log_path.read_text(encoding="utf-8", errors="replace")
files_checked.append("main.log")
for pattern in ["Undefined control sequence", "LaTeX Error", "Citation `", "Reference `"]:
if pattern in log_text and "undefined" in log_text.lower():
failed.append(f"Potential unresolved LaTeX issue in main.log: {pattern}")
if failed:
status = "FAIL"
elif external:
status = "BLOCKED_FOR_HUMAN"
else:
status = "PASS"
report = {
"overall_status": status,
"abstract_words": abstract_words,
"publication_units_estimate": round(publication_units, 3),
"counted_words_estimate": pu_words,
"figures": figures,
"tables": tables,
"failed_checks": failed,
"external_blockers": external,
"auto_fixable_items": auto_fixable,
"files_checked": sorted(set(files_checked)),
"commands_to_reproduce": [
"latexmk -pdf -interaction=nonstopmode -halt-on-error main.tex",
"python scripts/grl_readiness_check.py",
],
}
(OUT / "grl_ready_report.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
lines = [
"# GRL Readiness Report",
"",
f"- Overall status: `{status}`",
f"- Abstract words: {abstract_words}",
f"- Publication units estimate: {publication_units:.2f}",
f"- Counted words estimate: {pu_words}",
f"- Figures: {figures}",
f"- Tables: {tables}",
"",
"## Failed Checks",
"",
]
lines.extend([f"- {item}" for item in failed] or ["- None"])
lines.extend(["", "## External Blockers", ""])
lines.extend([f"- {item}" for item in external] or ["- None"])
lines.extend(["", "## Auto-Fixable Or Computation Items", ""])
lines.extend([f"- {item}" for item in auto_fixable] or ["- None"])
lines.extend(["", "## Commands", ""])
lines.extend([f"- `{cmd}`" for cmd in report["commands_to_reproduce"]])
(OUT / "grl_ready_report.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
print(json.dumps(report, indent=2))
return 0 if status in {"PASS", "BLOCKED_FOR_HUMAN"} else 1
if __name__ == "__main__":
raise SystemExit(main())