snr_bias / code /scripts /grl_final_submission_check.py
cangyeone's picture
Add open-data workflow and seed-fixed training manifests
0a31798 verified
Raw
History Blame Contribute Delete
10.9 kB
#!/usr/bin/env python3
"""Final submission-readiness checks for the GRL manuscript package."""
from __future__ import annotations
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MAIN = ROOT / "main.tex"
OUT = ROOT / "submission_final"
LOG = ROOT / "main.log"
PDF = ROOT / "main.pdf"
PLACEHOLDERS = [
"TBD",
"to be added",
"Requires author confirmation",
"email to be added",
"funding details require author confirmation",
"Acknowledgment and funding details require author confirmation",
"should be added before submission",
"public repository DOI has not yet been assigned",
"data DOI: TBD",
"code DOI: TBD",
"INSERT REAL",
"INSERT FINAL",
]
REQUIRED_ARCHIVE_TEXT = [
"snr_bias",
"Hugging Face",
"SeismicX-Cont",
"10.57967/hf/9006",
"SeisDispFusion-NCF",
"10.57967/hf/9114",
"CREDIT-X1local",
]
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",
]
REQUIRED_SECTIONS = [
"Plain Language Summary",
"Keywords",
"Introduction",
"Data and Methods",
"Results",
"Discussion and Conclusions",
"Open Research",
"Conflict of Interest declaration",
"AI Tool Disclosure",
]
EXPECTED_CITATIONS = [
"ross2018gpd",
"zhu2019phasenet",
"mousavi2020eqtransformer",
"li2024creditx1local",
"zhang2019real",
"yu2026seismicxcont",
"yu2026seisdispfusionncf",
"yu2026snrbias",
]
def read(path: Path) -> str:
return path.read_text(encoding="utf-8", errors="replace") if path.exists() else ""
def strip_comments(text: str) -> str:
return re.sub(r"(?<!\\)%.*", "", text)
def strip_latex(text: str) -> str:
text = strip_comments(text)
text = text.replace(r"\%", " percent ")
text = re.sub(r"\\(?:cite|citeA|citep|citet)\{[^}]*\}", " ", text)
text = re.sub(r"\\path\{([^}]*)\}", r" \1 ", text)
text = re.sub(r"\\texttt\{([^}]*)\}", r" \1 ", 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 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 captions(text: str) -> list[str]:
items = re.findall(r"\\caption\{(.*?)\}", text, re.S)
table_file = ROOT / "continuous_assoc_table.tex"
if table_file.exists():
items.extend(re.findall(r"\\caption\{(.*?)\}", table_file.read_text(encoding="utf-8"), re.S))
return items
def counted_text_for_pu(text: str) -> str:
parts = [extract_env(text, "abstract")]
after_begin = text.split(r"\begin{document}", 1)[-1]
before_bib = after_begin.split(r"\bibliography", 1)[0]
before_bib = re.sub(r"\\title\{.*?\}", " ", before_bib, flags=re.S)
before_bib = re.sub(r"\\authors\{.*?\}", " ", before_bib, flags=re.S)
before_bib = re.sub(r"\\affiliation\{.*?\}\{.*?\}", " ", before_bib, flags=re.S)
before_bib = re.sub(r"\\correspondingauthor\{.*?\}\{.*?\}", " ", before_bib, flags=re.S)
before_bib = re.sub(r"\\begin\{keypoints\}.*?\\end\{keypoints\}", " ", before_bib, flags=re.S)
before_bib = re.sub(r"\\section\*\{Keywords\}.*?(?=\\section|\Z)", " ", before_bib, flags=re.S)
before_bib = re.sub(r"\\begin\{table\}.*?\\end\{table\}", " ", before_bib, flags=re.S)
before_bib = re.sub(r"\\begin\{tabular\}.*?\\end\{tabular\}", " ", before_bib, flags=re.S)
before_bib = re.sub(r"\\begin\{figure\}.*?\\end\{figure\}", " ", before_bib, flags=re.S)
parts.append(before_bib)
parts.extend(captions(text))
return "\n".join(parts)
def unresolved_latex_issues(log_text: str) -> list[str]:
issues = []
patterns = [
"Undefined control sequence",
"LaTeX Error",
"Emergency stop",
"Fatal error",
"There were undefined references",
"There were undefined citations",
"Rerun to get cross-references right",
]
for pattern in patterns:
if pattern in log_text:
issues.append(pattern)
if re.search(r"LaTeX Warning: (Citation|Reference) `[^']+' undefined", log_text):
issues.append("undefined citation/reference warning")
return issues
def main() -> int:
OUT.mkdir(parents=True, exist_ok=True)
text = read(MAIN)
bib_text = read(ROOT / "references.bib")
log_text = read(LOG)
failures: list[str] = []
warnings: list[str] = []
human_input: list[str] = []
if not text:
failures.append("main.tex is missing or empty")
abstract_words = word_count(extract_env(text, "abstract"))
if abstract_words >= 150:
failures.append(f"Abstract is {abstract_words} words; GRL target is <150.")
kp = keypoints(text)
kp_lengths = [len(item) for item in kp]
if len(kp) > 3:
failures.append(f"Found {len(kp)} Key Points; maximum is 3.")
for idx, length in enumerate(kp_lengths, 1):
if length > 140:
failures.append(f"Key Point {idx} is {length} characters; maximum is 140.")
for token in PLACEHOLDERS:
if token.lower() in text.lower():
failures.append(f"Placeholder remains in manuscript: {token}")
for section in REQUIRED_SECTIONS:
if section not in text:
failures.append(f"Required section missing: {section}")
if "yuziye@cea-igp.ac.cn" not in text:
failures.append("Corresponding email is missing.")
if "Institute of Geophysics, China Earthquake Administration" not in text:
failures.append("Affiliation is missing.")
for required in REQUIRED_ARCHIVE_TEXT:
if required not in text:
failures.append(f"Open Research missing required archive text: {required}")
if "No specific funding was received for this work" not in text:
failures.append("Funding statement is missing or not finalized.")
if "no separate license file was present" in text:
human_input.append("Code archive license is not assigned; add a LICENSE file/metadata if a reuse license is intended.")
for figure in REQUIRED_FIGURES:
if not (ROOT / figure).exists():
failures.append(f"Required figure file missing: {figure}")
table_file = ROOT / "continuous_assoc_table.tex"
if not table_file.exists():
failures.append("continuous_assoc_table.tex is missing.")
else:
table_text = read(table_file)
for value in ["576,875", "1,301", "0.556", "1,561", "0.667"]:
if value not in table_text:
failures.append(f"Continuous association table missing expected value: {value}")
for citation in EXPECTED_CITATIONS:
if citation not in text:
failures.append(f"Expected citation is not used in main.tex: {citation}")
if citation not in bib_text:
failures.append(f"Expected citation is missing from references.bib: {citation}")
if not PDF.exists():
failures.append("main.pdf is missing.")
elif MAIN.exists() and PDF.stat().st_mtime < MAIN.stat().st_mtime:
failures.append("main.pdf is older than main.tex; recompile manuscript.")
latex_issues = unresolved_latex_issues(log_text)
if latex_issues:
failures.extend([f"LaTeX log issue: {issue}" for issue in latex_issues])
if not log_text:
warnings.append("main.log not found; compile before running final check for LaTeX-log validation.")
counted_words = word_count(counted_text_for_pu(text))
figure_count = len(re.findall(r"\\begin\{figure\}", text))
table_count = len(re.findall(r"\\begin\{table\}", text)) + len(re.findall(r"\\input\{continuous_assoc_table\}", text))
publication_units = counted_words / 500.0 + figure_count + table_count
if publication_units > 12:
failures.append(f"Publication units estimate is {publication_units:.2f}; maximum is 12.")
elif publication_units > 11.5:
warnings.append(f"Publication units estimate is {publication_units:.2f}; internal safety target is <=11.5.")
status = "FAIL" if failures else ("BLOCKED_FOR_HUMAN" if human_input else "PASS")
report = {
"overall_status": status,
"abstract_word_count": abstract_words,
"key_point_character_counts": kp_lengths,
"publication_units_estimate": round(publication_units, 3),
"counted_words_estimate": counted_words,
"figure_count": figure_count,
"table_count": table_count,
"compile_status": "checked_main_log" if log_text and not latex_issues else ("not_checked" if not log_text else "issues_found"),
"citation_status": "resolved" if not latex_issues else "check_log",
"placeholder_status": "clear" if not any(token.lower() in text.lower() for token in PLACEHOLDERS) else "found",
"open_research_status": "complete_required_dois_present",
"failures": failures,
"warnings": warnings,
"human_input_required": human_input,
}
(OUT / "final_submission_check.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
lines = [
"# Final Submission Check",
"",
f"- Overall status: `{status}`",
f"- Abstract word count: {abstract_words}",
f"- Key Point character counts: {', '.join(map(str, kp_lengths))}",
f"- Publication unit estimate: {publication_units:.2f}",
f"- Counted words estimate: {counted_words}",
f"- Figures: {figure_count}",
f"- Tables: {table_count}",
f"- Compile status: {report['compile_status']}",
f"- Citation status: {report['citation_status']}",
f"- Placeholder status: {report['placeholder_status']}",
f"- Open Research status: {report['open_research_status']}",
"",
"## Failures",
"",
*([f"- {item}" for item in failures] or ["- None"]),
"",
"## Warnings",
"",
*([f"- {item}" for item in warnings] or ["- None"]),
"",
"## Human Input Required",
"",
*([f"- {item}" for item in human_input] or ["- None"]),
]
(OUT / "final_submission_check.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
print(json.dumps(report, indent=2))
return 1 if status == "FAIL" else 0
if __name__ == "__main__":
raise SystemExit(main())