#!/usr/bin/env python3 """Check SNR/QC literature context and reference consistency.""" from __future__ import annotations import json import re from pathlib import Path ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / "submission_final" MAIN = ROOT / "main.tex" BIB = ROOT / "references.bib" LOG = ROOT / "main.log" ADDED_KEYS = [ "allen1978", "allen1982", "baer1987", "withers1998", "kalkan2016", "distefano2006", "perol2018convnetquake", "mousavi2019cred", "mousavi2019stead", "michelini2021instance", "woollam2022seisbench", "ni2023pnw", "munchmeyer2022picker", "myklebust2024regional", "zhao2023diting", "zhu2019denoising", "chen2019snr", "zhang2018dictionary", "shapiro2004noise", "shapiro2005tomography", "bensen2007ambient", "yao2008tibet", "lin2008westernus", "lin2009eikonal", "zhang2020dispersion", "dai2020dispersion", "wang2021dispersion", "jiang2023surfnet", ] def read(path: Path) -> str: return path.read_text(encoding="utf-8", errors="replace") if path.exists() else "" def strip_latex(text: str) -> str: text = re.sub(r"(? int: return len(re.findall(r"[A-Za-z0-9]+(?:[-'][A-Za-z0-9]+)?", strip_latex(text))) 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 citation_keys(text: str) -> set[str]: keys: set[str] = set() for match in re.finditer(r"\\cite[A-Za-z]*\{([^}]+)\}", text): for key in match.group(1).split(","): keys.add(key.strip()) return keys def bib_keys(text: str) -> set[str]: return set(re.findall(r"@\w+\{([^,\s]+)", text)) def counted_text_for_pu(text: str) -> str: parts = [extract_env(text, "abstract")] body = text.split(r"\begin{document}", 1)[-1].split(r"\bibliography", 1)[0] body = re.sub(r"\\title\{.*?\}", " ", body, flags=re.S) body = re.sub(r"\\authors\{.*?\}", " ", body, flags=re.S) body = re.sub(r"\\affiliation\{.*?\}\{.*?\}", " ", body, flags=re.S) body = re.sub(r"\\correspondingauthor\{.*?\}\{.*?\}", " ", body, flags=re.S) body = re.sub(r"\\begin\{keypoints\}.*?\\end\{keypoints\}", " ", body, flags=re.S) body = re.sub(r"\\section\*\{Keywords\}.*?(?=\\section|\Z)", " ", body, flags=re.S) captions = re.findall(r"\\caption\{(.*?)\}", body, re.S) table_file = ROOT / "continuous_assoc_table.tex" if table_file.exists(): captions.extend(re.findall(r"\\caption\{(.*?)\}", read(table_file), re.S)) body = re.sub(r"\\begin\{figure\}.*?\\end\{figure\}", " ", body, flags=re.S) body = re.sub(r"\\begin\{table\}.*?\\end\{table\}", " ", body, flags=re.S) body = re.sub(r"\\begin\{tabular\}.*?\\end\{tabular\}", " ", body, flags=re.S) return "\n".join([body, *captions, *parts]) def main() -> int: OUT.mkdir(parents=True, exist_ok=True) text = read(MAIN) bib = read(BIB) log = read(LOG) failures: list[str] = [] warnings: list[str] = [] bkeys = bib_keys(bib) ckeys = citation_keys(text) total_refs = len(bkeys) if not (30 <= total_refs <= 45): failures.append(f"Total reference count is {total_refs}; expected 30--45.") missing_from_bib = sorted(ckeys - bkeys) if missing_from_bib: failures.append(f"Cited keys missing from bibliography: {', '.join(missing_from_bib)}") missing_added = sorted(set(ADDED_KEYS) - bkeys) if missing_added: failures.append(f"Added keys missing from bibliography: {', '.join(missing_added)}") uncited_added = sorted(set(ADDED_KEYS) - ckeys) if uncited_added: failures.append(f"Added bibliography entries not cited: {', '.join(uncited_added)}") if log: if "There were undefined references" in log or "There were undefined citations" in log: failures.append("LaTeX log reports undefined references or citations.") if re.search(r"LaTeX Warning: (Citation|Reference) `[^']+' undefined", log): failures.append("LaTeX log contains undefined citation/reference warning.") else: warnings.append("main.log is absent; compile before checking unresolved citations.") intro_match = re.search(r"\\section\{Introduction\}(.*?)(?=\\section\{Data and Methods\})", text, re.S) intro = intro_match.group(1).lower() if intro_match else "" concepts = { "signal-quality measures": "signal-quality measures", "STA/LTA or amplitude-ratio": ["sta/lta", "amplitude-ratio"], "SNR or trace-quality metadata": ["snr or trace-quality metadata", "trace-quality metadata"], "denoising or filtering": ["denoising", "filtering"], "ambient-noise processing": "ambient-noise processing", "quality control": "quality control", "dispersion measurements": "dispersion measurements", "training-distribution question": "training-distribution question", } missing_concepts = [] for label, needles in concepts.items(): if isinstance(needles, str): needles = [needles] if not any(needle in intro for needle in needles): missing_concepts.append(label) if missing_concepts: failures.append(f"Introduction missing required SNR/QC concepts: {', '.join(missing_concepts)}") overclaims = ["all pipelines", "every seismic ai workflow", "always harmful", "proved", "universally"] found_overclaims = [phrase for phrase in overclaims if phrase in text.lower()] if found_overclaims: failures.append(f"Unsupported overclaim phrases remain: {', '.join(found_overclaims)}") abstract_words = word_count(extract_env(text, "abstract")) if abstract_words >= 150: failures.append(f"Abstract has {abstract_words} words; must be <150.") keypoints = [strip_latex(item).strip() for item in re.findall(r"\\begin\{keypoints\}(.*?)\\end\{keypoints\}", text, re.S)[0].split(r"\item") if item.strip()] keypoint_lengths = [len(item) for item in keypoints] if len(keypoints) > 3: failures.append(f"Found {len(keypoints)} key points; maximum is 3.") for idx, length in enumerate(keypoint_lengths, 1): if length > 140: failures.append(f"Key Point {idx} has {length} characters; maximum is 140.") 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)) pu = counted_words / 500.0 + figure_count + table_count if pu > 12: failures.append(f"Publication units estimate is {pu:.2f}; maximum is 12.") report = { "overall_status": "PASS" if not failures else "FAIL", "total_reference_count": total_refs, "added_reference_count": len(ADDED_KEYS), "uncited_added_references": uncited_added, "missing_cited_keys_from_bib": missing_from_bib, "abstract_word_count": abstract_words, "key_point_character_counts": keypoint_lengths, "publication_units_estimate": round(pu, 3), "counted_words_estimate": counted_words, "failures": failures, "warnings": warnings, } (OUT / "snr_reference_context_check.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") lines = [ "# SNR Reference Context Check", "", f"- Overall status: `{report['overall_status']}`", f"- Total references: {total_refs}", f"- Added references: {len(ADDED_KEYS)}", f"- Abstract words: {abstract_words}", f"- Key Point character counts: {', '.join(map(str, keypoint_lengths))}", f"- Publication unit estimate: {pu:.2f}", "", "## Failures", "", *([f"- {item}" for item in failures] or ["- None"]), "", "## Warnings", "", *([f"- {item}" for item in warnings] or ["- None"]), ] (OUT / "snr_reference_context_check.md").write_text("\n".join(lines) + "\n", encoding="utf-8") print(json.dumps(report, indent=2)) return 0 if not failures else 1 if __name__ == "__main__": raise SystemExit(main())