""" reproducibility.py — Missing-information detection and reproducibility scoring. Scores are heuristic and evidence-based. The goal is to produce a useful local candidate before Nemotron refinement, while avoiding overconfident scores when PDF extraction evidence is noisy. """ from __future__ import annotations import re from typing import Any, Dict, List, Tuple # --------------------------------------------------------------------------- # Text helpers # --------------------------------------------------------------------------- def _clean(text: str) -> str: text = text or "" text = text.replace("\x00", " ").replace("\u00a0", " ") text = re.sub(r"\s+", " ", text) text = re.sub(r"\b10\.\d{4,9}/[-._;()/:A-Za-z0-9]+", "", text) return text.strip(" .;:\n\t") def _joined_text(extracted: Dict[str, Any]) -> str: parts: List[str] = [] for sec in extracted.get("sections", []) or []: if sec.get("role") in {"references", "appendix", "boilerplate"}: continue parts.append(str(sec.get("title", ""))) parts.append(str(sec.get("text", ""))) return _clean("\n".join(parts)).lower() def _has_any(text: str, terms: List[str]) -> bool: return any(t.lower() in text for t in terms) def _matched_terms(text: str, terms: List[str], limit: int = 5) -> List[str]: return [t for t in terms if t.lower() in text][:limit] # --------------------------------------------------------------------------- # Paper-type-specific reproducibility checks # --------------------------------------------------------------------------- def _check_items(paper_type: str) -> Dict[str, List[str]]: if paper_type == "systematic_review": return { "search databases specified": [ "pubmed", "scopus", "web of knowledge", "eric", "cochrane", "database", "databases", ], "search date range specified": [ "january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december", "between", "from", "until", "to january", "published between", ], "inclusion criteria specified": [ "inclusion criteria", "eligibility criteria", "eligible studies", ], "exclusion criteria specified": [ "exclusion criteria", "excluded", "not being", "were excluded", ], "screening process specified": [ "screened", "screening", "titles and abstracts", "two independent", "reviewers", "duplicates", "endnote", ], "quality assessment specified": [ "quality assessment", "risk of bias", "best evidence medical education", "valid tool", "critical appraisal", "assessment tool", ], "number of included studies specified": [ "included", "enrolled", "final review", "studies were included", "articles were included", "10 articles", "ten studies", ], } if paper_type == "machine_learning": return { "dataset details specified": [ "dataset", "training set", "test set", "validation set", "benchmark", "corpus", "samples", "instances", ], "train/validation/test split specified": [ "train", "validation", "test", "split", "dev set", "development set", ], "model architecture specified": [ "architecture", "layers", "encoder", "decoder", "transformer", "cnn", "resnet", "bert", "attention", "feed-forward", ], "hyperparameters specified": [ "learning rate", "batch size", "epochs", "optimizer", "dropout", "weight decay", "warmup", "scheduler", ], "hardware specified": [ "gpu", "tpu", "cuda", "p100", "v100", "a100", "nvidia", ], "evaluation metrics specified": [ "accuracy", "f1", "auc", "bleu", "rouge", "perplexity", "rmse", "mae", "precision", "recall", ], "code availability specified": [ "github", "code", "repository", "available at", "source code", ], "random seed specified": ["random seed", "seed"], } if paper_type == "clinical_study": return { "cohort or participants specified": [ "patients", "participants", "cohort", "subjects", "population", ], "inclusion criteria specified": ["inclusion criteria", "eligible"], "exclusion criteria specified": ["exclusion criteria", "excluded"], "outcomes specified": ["outcome", "endpoint", "mortality", "diagnosis"], "statistical analysis specified": [ "statistical analysis", "p-value", "confidence interval", "regression", ], "ethics approval specified": [ "ethics", "institutional review", "informed consent", "irb", ], } return { "data/source details specified": [ "data", "dataset", "source", "samples", "studies", "articles", ], "method/procedure specified": [ "method", "procedure", "approach", "experiment", "analysis", ], "evaluation or analysis specified": [ "evaluation", "result", "metric", "analysis", "measured", "assessed", ], "limitations discussed": ["limitation", "limitations", "future work"], } # --------------------------------------------------------------------------- # Evidence quality / noise handling # --------------------------------------------------------------------------- _NOISY_EVIDENCE_MARKERS = [ "the there", "being accordingly", "endnote teachers", "resultsare", "analysis of the resultsare", "table 2:", "department of", "university of", "medical sciences", "corresponding author", "access this article online", "how to cite", "need this systematic review", "the that", ] def _roadmap_blob(paper_card: Dict[str, Any]) -> str: roadmap = paper_card.get("reproduction_roadmap") or {} parts: List[str] = [] for key in [ "datasets", "software_requirements", "experimental_steps", "evaluation_procedure", "expected_outputs", "missing_for_reproduction", ]: value = roadmap.get(key, []) if isinstance(value, list): for item in value: if isinstance(item, dict): parts.extend(str(v) for v in item.values()) else: parts.append(str(item)) elif value: parts.append(str(value)) return _clean(" ".join(parts)).lower() def _noise_report(extracted: Dict[str, Any], paper_card: Dict[str, Any]) -> Tuple[int, List[str]]: """Return count and examples of noisy evidence markers.""" blob = _roadmap_blob(paper_card) if not blob: # Fallback to body text only if roadmap is not yet attached. blob = _joined_text(extracted) found = [m for m in _NOISY_EVIDENCE_MARKERS if m in blob] # Extra generic noise signals. if len(re.findall(r"\[\d+", blob)) >= 12: found.append("many citation fragments") if re.search(r"\b(the|and|of)\s+\1\b", blob): found.append("repeated function-word artifact") return len(found), found[:8] def _apply_score_caps( paper_type: str, score: float, missing: List[str], extracted: Dict[str, Any], paper_card: Dict[str, Any], ) -> Tuple[float, List[str], Dict[str, Any]]: """Prevent misleadingly high scores when evidence is noisy or incomplete.""" diagnostics: Dict[str, Any] = {} noise_count, noise_examples = _noise_report(extracted, paper_card) diagnostics["noise_count"] = noise_count diagnostics["noise_examples"] = noise_examples if noise_count > 0: msg = "some extracted evidence appears noisy due to PDF layout" if msg not in missing: missing.append(msg) # Systematic reviews should not get 1.0 if roadmap/evidence is visibly noisy. if paper_type == "systematic_review": if noise_count >= 3: score = min(score, 0.65) elif noise_count >= 1: score = min(score, 0.75) roadmap = paper_card.get("reproduction_roadmap") or {} if not roadmap.get("experimental_steps"): score = min(score, 0.70) if not roadmap.get("evaluation_procedure"): score = min(score, 0.70) # ML papers need either hyperparameters or code/hardware to be strong. if paper_type == "machine_learning": text = _joined_text(extracted) has_hparams = _has_any(text, ["learning rate", "batch size", "optimizer", "dropout", "epoch"]) has_code = _has_any(text, ["github", "repository", "code available", "source code"]) has_hardware = _has_any(text, ["gpu", "tpu", "cuda", "p100", "v100", "a100"]) if not has_hparams: score = min(score, 0.80) if not has_code and not has_hardware: score = min(score, 0.85) return round(score, 3), missing, diagnostics def _score_level(score: float) -> str: if score >= 0.80: return "strong" if score >= 0.50: return "partial" return "weak" # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- def reproducibility_report(extracted: Dict[str, Any], paper_card: Dict[str, Any]) -> Dict[str, Any]: paper_type = paper_card.get("paper_type", "general_research") text = _joined_text(extracted) checks = _check_items(paper_type) detected: List[str] = [] missing: List[str] = [] evidence: Dict[str, List[str]] = {} for label, terms in checks.items(): if _has_any(text, terms): detected.append(label) evidence[label] = _matched_terms(text, terms) else: missing.append(label) # Candidate-card overrides for generic papers. if paper_card.get("datasets_or_data_sources") and "data/source details specified" in missing: missing.remove("data/source details specified") detected.append("data/source details specified") evidence["data/source details specified"] = ["paper_card.datasets_or_data_sources"] if paper_card.get("metrics_or_measurements") and "evaluation or analysis specified" in missing: missing.remove("evaluation or analysis specified") detected.append("evaluation or analysis specified") evidence["evaluation or analysis specified"] = ["paper_card.metrics_or_measurements"] total = max(1, len(checks)) score = len(detected) / total score, missing, diagnostics = _apply_score_caps( paper_type=paper_type, score=score, missing=missing, extracted=extracted, paper_card=paper_card, ) # Deduplicate while preserving order. detected = list(dict.fromkeys(detected)) missing = list(dict.fromkeys(missing)) return { "paper_type": paper_type, "score": score, "level": _score_level(score), "detected_items": detected, "missing_items": missing, "evidence_terms": evidence, "diagnostics": diagnostics, }