| |
| """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"(?<!\\)%.*", " ", text) |
| text = text.replace(r"\%", " percent ") |
| text = re.sub(r"\\(?:cite|citeA|citep|citet)\{[^}]*\}", " ", text) |
| text = re.sub(r"\\(?:path|texttt|textit)\{([^}]*)\}", 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: |
| 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()) |
|
|