#!/usr/bin/env python3
"""Build frozen evidence tables for the MVA manuscript.

The script intentionally separates evidence with different datasets, evaluation
caps, seeds, and development/test roles. It never silently pools incompatible
experiments.
"""

from __future__ import annotations

import csv
import json
import math
import statistics
from pathlib import Path


SCRIPT_DIR = Path(__file__).resolve().parent
MVA_ROOT = SCRIPT_DIR.parent
WORKSPACE = MVA_ROOT.parents[2]
RESULTS = WORKSPACE / "论文实践" / "05_远端结果待回传"
FINAL_REVISION = RESULTS / "tipcell_mva_final_revision"
TABLES = MVA_ROOT / "tables"
EVIDENCE = MVA_ROOT / "evidence"

METHOD_LABELS = {
    "coco_full": "COCO-full",
    "imagenet_coco": "ImageNet+COCO",
    "simclr_coco": "SimCLR+COCO",
    "supcon_coco": "SupCon+COCO",
}


def read_csv(path: Path) -> list[dict[str, str]]:
    with path.open(encoding="utf-8-sig", newline="") as handle:
        return list(csv.DictReader(handle))


def write_csv(path: Path, rows: list[dict], fieldnames: list[str] | None = None) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    if not rows:
        raise ValueError(f"Refusing to write empty CSV: {path}")
    names = fieldnames or list(rows[0])
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=names)
        writer.writeheader()
        writer.writerows(rows)


def finite(value: float, label: str) -> float:
    if not math.isfinite(value):
        raise ValueError(f"Non-finite value for {label}: {value}")
    return value


def sample_sd(values: list[float]) -> float:
    return statistics.stdev(values) if len(values) > 1 else 0.0


def percentile(values: list[float], percentage: float) -> float:
    ordered = sorted(values)
    if not ordered:
        raise ValueError("Cannot calculate a percentile of an empty sequence")
    position = (len(ordered) - 1) * percentage / 100.0
    lower = math.floor(position)
    upper = math.ceil(position)
    if lower == upper:
        return float(ordered[lower])
    weight = position - lower
    return float(ordered[lower] * (1.0 - weight) + ordered[upper] * weight)


def build_primary_tables() -> tuple[list[dict], list[dict]]:
    source = EVIDENCE / "seed44_remote_raw" / "collected"
    summary = read_csv(source / "official_test_summary_n3.csv")
    paired = read_csv(source / "official_test_paired_differences_n3.csv")
    if len(summary) != 4 or any(int(row["n_seeds"]) != 3 for row in summary):
        raise ValueError("Primary LIVECell table is not a complete 4-method x 3-seed table")
    write_csv(TABLES / "table_primary_livecell_test_n3.csv", summary)
    write_csv(TABLES / "table_primary_paired_differences_n3.csv", paired)
    return summary, paired


def dense_csv_paths() -> list[Path]:
    old_root = (
        RESULTS
        / "revision_closure_2026-07-20"
        / "tipcell_revision_evaluation_closure"
    )
    paths: list[Path] = []
    for seed in (42, 43):
        for method in METHOD_LABELS:
            paths.append(
                old_root
                / f"test_{method}_s{seed}"
                / "protocol_ablation"
                / f"{method}_seed{seed}_dense_protocol_ablation.csv"
            )
    new_root = EVIDENCE / "seed44_remote_raw"
    run_dirs = {
        "coco_full": "coco_full_e20_s44_bblr1",
        "imagenet_coco": "imagenet_coco_e20_s44_bblr5",
        "simclr_coco": "simclr_coco_e20_s44_bblr5",
        "supcon_coco": "supcon_coco_e20_s44_bblr5",
    }
    for method, run_dir in run_dirs.items():
        paths.append(
            new_root
            / run_dir
            / "test_official"
            / "protocol_ablation"
            / f"{method}_seed44_dense_protocol_ablation.csv"
        )
    for path in paths:
        if not path.is_file():
            raise FileNotFoundError(path)
    return paths


def build_density_n3() -> list[dict]:
    grouped: dict[tuple[str, str], list[float]] = {}
    image_counts: dict[str, int] = {}
    seen: set[tuple[str, int, str]] = set()
    for path in dense_csv_paths():
        for row in read_csv(path):
            if row["iou_type"] != "segm" or int(row["max_dets"]) != 3000:
                continue
            method, seed, density = row["method"], int(row["seed"]), row["density"]
            key = (method, seed, density)
            if key in seen:
                raise ValueError(f"Duplicate density row: {key}")
            seen.add(key)
            grouped.setdefault((method, density), []).append(finite(float(row["AP"]), str(key)))
            image_counts[density] = int(row["num_images"])
    rows: list[dict] = []
    density_order = ["all", "low", "medium", "high", "extreme"]
    for method in METHOD_LABELS:
        for density in density_order:
            values = grouped[(method, density)]
            if len(values) != 3:
                raise ValueError(f"Expected 3 seeds for {(method, density)}, got {len(values)}")
            rows.append(
                {
                    "method": METHOD_LABELS[method],
                    "method_key": method,
                    "density": density,
                    "num_images": image_counts[density],
                    "n_seeds": 3,
                    "segm_AP_mean": statistics.mean(values),
                    "segm_AP_sample_SD": sample_sd(values),
                    "seed42": values[0],
                    "seed43": values[1],
                    "seed44": values[2],
                }
            )
    write_csv(TABLES / "table_density_strata_n3.csv", rows)
    return rows


def maxdets_seed44_paths() -> list[Path]:
    root = EVIDENCE / "seed44_remote_raw"
    run_dirs = {
        "coco_full": "coco_full_e20_s44_bblr1",
        "imagenet_coco": "imagenet_coco_e20_s44_bblr5",
        "simclr_coco": "simclr_coco_e20_s44_bblr5",
        "supcon_coco": "supcon_coco_e20_s44_bblr5",
    }
    paths: list[Path] = []
    for method, run_dir in run_dirs.items():
        output = root / run_dir / "test_official" / "protocol_ablation_maxdets_only"
        csv_path = output / f"{method}_seed44_maxdets_only.csv"
        audit_path = output / f"{method}_seed44_maxdets_only_audit.json"
        if not csv_path.is_file() or not audit_path.is_file():
            raise FileNotFoundError(f"Missing seed44 maxDets evidence for {method}")
        with audit_path.open(encoding="utf-8") as handle:
            audit = json.load(handle)
        gate = audit.get("formal_reproduction_gate", {})
        if audit.get("status") != "ok" or gate.get("passed") is not True:
            raise ValueError(f"Failed seed44 maxDets audit for {method}: {audit_path}")
        if abs(float(gate["absolute_error"])) > float(gate["tolerance"]):
            raise ValueError(f"AP@3000 reproduction error exceeds tolerance: {audit_path}")
        paths.append(csv_path)
    return paths


def build_maxdets_n3() -> list[dict]:
    grouped: dict[tuple[str, int], dict[str, list[float]]] = {}
    for path in [*dense_csv_paths()[:8], *maxdets_seed44_paths()]:
        for row in read_csv(path):
            if row["iou_type"] != "segm" or row["density"] != "all":
                continue
            key = (row["method"], int(row["max_dets"]))
            metrics = grouped.setdefault(key, {"AP": [], "AR": []})
            metrics["AP"].append(finite(float(row["AP"]), f"{key}:AP"))
            metrics["AR"].append(finite(float(row["AR"]), f"{key}:AR"))
    rows: list[dict] = []
    for method in METHOD_LABELS:
        for cap in (100, 300, 1000, 3000):
            ap_values = grouped[(method, cap)]["AP"]
            ar_values = grouped[(method, cap)]["AR"]
            if len(ap_values) != 3 or len(ar_values) != 3:
                raise ValueError(
                    f"Expected 3 AP/AR seeds for {(method, cap)}, "
                    f"got AP={len(ap_values)} AR={len(ar_values)}"
                )
            rows.append(
                {
                    "method": METHOD_LABELS[method],
                    "method_key": method,
                    "max_dets": cap,
                    "n_seeds": 3,
                    "segm_AP_mean": statistics.mean(ap_values),
                    "segm_AP_sample_SD": sample_sd(ap_values),
                    "segm_AR_mean": statistics.mean(ar_values),
                    "segm_AR_sample_SD": sample_sd(ar_values),
                    "AP_seed42": ap_values[0],
                    "AP_seed43": ap_values[1],
                    "AP_seed44": ap_values[2],
                    "AR_seed42": ar_values[0],
                    "AR_seed43": ar_values[1],
                    "AR_seed44": ar_values[2],
                }
            )
    write_csv(TABLES / "table_maxdets_ablation_n3.csv", rows)
    return rows


def load_summary_ap(path: Path) -> float:
    with path.open(encoding="utf-8") as handle:
        data = json.load(handle)
    if data.get("status") != "ok":
        raise ValueError(f"Run is not valid: {path}")
    return finite(float(data["eval_summary"]["segm"]["AP"]), str(path))


def build_cellpose_n3() -> list[dict]:
    methods = {
        "ImageNet backbone + random detector components": [
            RESULTS / "cellpose_external" / "cellpose_imagenet_bb_5pct_s42" / "summary.json",
            RESULTS / "cellpose_multiseed" / "cellpose_imagenet_bb_5pct_s43" / "summary.json",
            RESULTS / "cellpose_multiseed" / "cellpose_imagenet_bb_5pct_s44" / "summary.json",
        ],
        "ImageNet backbone + COCO detector components": [
            RESULTS / "cellpose_external" / "cellpose_imagenet_coco_5pct_s42" / "summary.json",
            RESULTS / "cellpose_multiseed" / "cellpose_imagenet_coco_5pct_s43" / "summary.json",
            RESULTS / "cellpose_multiseed" / "cellpose_imagenet_coco_5pct_s44" / "summary.json",
        ],
    }
    coco_full_paths = [
        FINAL_REVISION / f"cellpose_coco_full_e20_s{seed}" / "summary.json"
        for seed in (42, 43, 44)
    ]
    if all(path.is_file() for path in coco_full_paths):
        methods["COCO-full detector"] = coco_full_paths
    rows: list[dict] = []
    raw: dict[str, list[float]] = {}
    for method, paths in methods.items():
        values = [load_summary_ap(path) for path in paths]
        raw[method] = values
        rows.append(
            {
                "method": method,
                "n_seeds": 3,
                "segm_AP_mean": statistics.mean(values),
                "segm_AP_sample_SD": sample_sd(values),
                "seed42": values[0],
                "seed43": values[1],
                "seed44": values[2],
                "train_images": 27,
                "evaluation_images": 68,
                "max_instances_per_train_image": 128,
                "max_dets": 100,
            }
        )
    deltas = [
        b - a
        for a, b in zip(
            raw["ImageNet backbone + random detector components"],
            raw["ImageNet backbone + COCO detector components"],
        )
    ]
    rows.append(
        {
            "method": "Paired difference: COCO - random detector components",
            "n_seeds": 3,
            "segm_AP_mean": statistics.mean(deltas),
            "segm_AP_sample_SD": sample_sd(deltas),
            "seed42": deltas[0],
            "seed43": deltas[1],
            "seed44": deltas[2],
            "train_images": 27,
            "evaluation_images": 68,
            "max_instances_per_train_image": 128,
            "max_dets": 100,
        }
    )
    write_csv(TABLES / "table_cellpose_external_n3.csv", rows)
    return rows


def build_cellpose_split_manifest() -> None:
    source = RESULTS / "cellpose_external"
    train_path = source / "cellpose_train_5pct.json"
    eval_path = source / "cellpose_val.json"
    stats_path = source / "cellpose_stats.json"
    for path in (train_path, eval_path, stats_path):
        if not path.is_file():
            raise FileNotFoundError(path)

    rows = []
    split_data: dict[str, dict] = {}
    split_counts: dict[str, list[int]] = {}
    for split, path in (("train_5pct", train_path), ("evaluation", eval_path)):
        data = json.loads(path.read_text(encoding="utf-8"))
        split_data[split] = data
        counts: dict[int, int] = {}
        for annotation in data["annotations"]:
            image_id = int(annotation["image_id"])
            counts[image_id] = counts.get(image_id, 0) + 1
        split_counts[split] = [counts.get(int(image["id"]), 0) for image in data["images"]]
        for image in data["images"]:
            image_id = int(image["id"])
            gt_instances = counts.get(image_id, 0)
            rows.append(
                {
                    "split": split,
                    "image_id": image_id,
                    "file_name": image["file_name"],
                    "width": int(image["width"]),
                    "height": int(image["height"]),
                    "gt_instances": gt_instances,
                    "instances_used_for_training": (
                        min(gt_instances, 128) if split == "train_5pct" else "not_applicable"
                    ),
                    "instances_used_by_coco_evaluation": (
                        "not_applicable" if split == "train_5pct" else gt_instances
                    ),
                }
            )
    write_csv(EVIDENCE / "cellpose_split_file_manifest.csv", rows)
    stats = json.loads(stats_path.read_text(encoding="utf-8"))
    train_areas: dict[int, list[float]] = {}
    for annotation in split_data["train_5pct"]["annotations"]:
        train_areas.setdefault(int(annotation["image_id"]), []).append(
            float(annotation.get("area", 0.0))
        )
    retained_areas: list[float] = []
    excluded_areas: list[float] = []
    for areas in train_areas.values():
        ordered = sorted(areas, reverse=True)
        retained_areas.extend(ordered[:128])
        excluded_areas.extend(ordered[128:])
    train_counts = split_counts["train_5pct"]
    evaluation_counts = split_counts["evaluation"]
    audit = {
        "status": "ok",
        "source_subset": "Cellpose public 2D train/test image-mask pairs",
        "train_population_images": int(stats["train_all"]["images"]),
        "train_selection_rule": (
            "sort numeric *_img.png IDs, then random.sample 5% with Python random seed 42"
        ),
        "split_seed_role": "used once to construct the fixed 27-image subset",
        "model_training_seeds": [42, 43, 44],
        "train_selected_images": int(stats["train_5pct"]["images"]),
        "evaluation_selection_rule": "all 68 public 2D test image-mask pairs",
        "evaluation_images": int(stats["val"]["images"]),
        "training_instance_cap": 128,
        "training_instance_selection_rule": (
            "sort annotations by recorded area descending and retain the 128 largest; "
            "fixed across conditions and model-training seeds"
        ),
        "train_annotations_total": sum(train_counts),
        "train_images_over_128": sum(value > 128 for value in train_counts),
        "train_annotations_retained": len(retained_areas),
        "train_annotations_excluded": len(excluded_areas),
        "train_excluded_fraction": len(excluded_areas) / sum(train_counts),
        "retained_area_median_px2": statistics.median(retained_areas),
        "excluded_area_median_px2": statistics.median(excluded_areas),
        "inference_and_cocoeval_max_dets": 100,
        "evaluation_ground_truth": "all annotations in cellpose_val.json",
        "evaluation_annotations_total": sum(evaluation_counts),
        "evaluation_gt_count_median": statistics.median(evaluation_counts),
        "evaluation_gt_count_p90": percentile(evaluation_counts, 90),
        "evaluation_gt_count_p99": percentile(evaluation_counts, 99),
        "evaluation_gt_count_max": max(evaluation_counts),
        "evaluation_images_over_100_gt": sum(value > 100 for value in evaluation_counts),
        "evaluation_images_over_128_gt": sum(value > 128 for value in evaluation_counts),
        "evaluation_cap_interpretation": (
            "AP@100 is a shared capped comparison and not an uncapped Cellpose benchmark"
        ),
        "train_annotation_file": str(train_path.resolve()),
        "evaluation_annotation_file": str(eval_path.resolve()),
        "manifest": str((EVIDENCE / "cellpose_split_file_manifest.csv").resolve()),
    }
    (EVIDENCE / "cellpose_split_audit.json").write_text(
        json.dumps(audit, ensure_ascii=False, indent=2), encoding="utf-8"
    )


def copy_control_tables() -> None:
    source = RESULTS / "second_review_closure_2026-07-21" / "collected"
    for name in (
        "matched_random_raw.csv",
        "matched_random_summary.csv",
        "matched_random_deltas_by_seed.csv",
    ):
        rows = read_csv(source / name)
        write_csv(TABLES / name, rows)

    raw = read_csv(source / "factorial_100epoch_raw.csv")
    seed44_path = FINAL_REVISION / "factorial_100epoch_seed44_raw.csv"
    if seed44_path.is_file():
        seed44 = read_csv(seed44_path)
        expected = {
            ("COCO-full", 1, 44),
            ("COCO-full", 5, 44),
            ("SimCLR+COCO", 1, 44),
            ("SimCLR+COCO", 5, 44),
        }
        observed = {
            (row["backbone"], int(row["backbone_lr_multiplier"]), int(row["seed"]))
            for row in seed44
        }
        if observed != expected:
            raise ValueError(f"Incomplete seed44 factorial: {sorted(observed)}")
        raw.extend(seed44)
    raw.sort(key=lambda row: (row["backbone"], int(row["backbone_lr_multiplier"]), int(row["seed"])))
    write_csv(TABLES / "factorial_100epoch_raw.csv", raw)

    grouped: dict[tuple[str, int], list[float]] = {}
    by_seed: dict[int, dict[tuple[str, int], float]] = {}
    for row in raw:
        backbone = row["backbone"]
        multiplier = int(row["backbone_lr_multiplier"])
        seed = int(row["seed"])
        ap = finite(float(row["AP"]), f"factorial:{backbone}:{multiplier}:{seed}")
        grouped.setdefault((backbone, multiplier), []).append(ap)
        by_seed.setdefault(seed, {})[(backbone, multiplier)] = ap
    summary_rows = [
        {
            "backbone": backbone,
            "backbone_lr_multiplier": multiplier,
            "n_seeds": len(values),
            "AP_mean": statistics.mean(values),
            "AP_sample_SD": sample_sd(values),
        }
        for (backbone, multiplier), values in sorted(grouped.items())
    ]
    write_csv(TABLES / "factorial_100epoch_summary.csv", summary_rows)

    effect_rows = []
    expected_cells = {
        ("COCO-full", 1), ("COCO-full", 5),
        ("SimCLR+COCO", 1), ("SimCLR+COCO", 5),
    }
    for seed, values in sorted(by_seed.items()):
        if set(values) != expected_cells:
            raise ValueError(f"Incomplete factorial cells for seed {seed}: {sorted(values)}")
        coco_lr = values[("COCO-full", 5)] - values[("COCO-full", 1)]
        simclr_lr = values[("SimCLR+COCO", 5)] - values[("SimCLR+COCO", 1)]
        effect_rows.append(
            {
                "seed": seed,
                "simclr_minus_coco_at_1x": (
                    values[("SimCLR+COCO", 1)] - values[("COCO-full", 1)]
                ),
                "simclr_minus_coco_at_5x": (
                    values[("SimCLR+COCO", 5)] - values[("COCO-full", 5)]
                ),
                "lr_5x_minus_1x_for_coco": coco_lr,
                "lr_5x_minus_1x_for_simclr": simclr_lr,
                "difference_in_differences": simclr_lr - coco_lr,
            }
        )
    write_csv(TABLES / "factorial_100epoch_effects_by_seed.csv", effect_rows)
    bootstrap = RESULTS / "second_review_test_bootstrap_2026-07-22" / "paired_bootstrap_summary.csv"
    write_csv(TABLES / "paired_bootstrap_summary_n2_fixed_models.csv", read_csv(bootstrap))


def build_registry() -> list[dict]:
    rows = [
        {
            "evidence": "Official LIVECell 5% test comparison",
            "dataset": "LIVECell",
            "role": "official test",
            "replication": "4 methods x seeds 42/43/44",
            "protocol": "all instances; maxDets=3000; 20 epochs",
            "paper_use": "MAIN",
            "reason": "Complete three-seed primary endpoint with frozen development choices",
        },
        {
            "evidence": "Density-stratified official test",
            "dataset": "LIVECell",
            "role": "official test",
            "replication": "4 methods x 3 seeds",
            "protocol": "maxDets=3000; four predeclared density strata",
            "paper_use": "MAIN",
            "reason": "Three-seed evidence that raising the cap does not remove dense-scene degradation",
        },
        {
            "evidence": "maxDets response",
            "dataset": "LIVECell",
            "role": "official test",
            "replication": "4 methods x seeds 42/43/44",
            "protocol": "maxDets=100/300/1000/3000",
            "paper_use": "MAIN",
            "reason": "Three-seed cap response; every seed44 AP@3000 value exactly reproduced the formal endpoint",
        },
        {
            "evidence": "Matched random detector components",
            "dataset": "LIVECell",
            "role": "development validation",
            "replication": "4 backbones x seeds 42/43",
            "protocol": "matched FrozenBN, random FPN/RPN/RoI, optimizer groups",
            "paper_use": "MAIN",
            "reason": "Separates backbone source from transferred detector components",
        },
        {
            "evidence": "100-epoch backbone x LR factorial",
            "dataset": "LIVECell",
            "role": "development validation",
            "replication": "2 backbones x 2 LR multipliers x seeds 42/43/44",
            "protocol": "COCO/SimCLR x 1x/5x; 100 epochs",
            "paper_use": "MAIN_DESCRIPTIVE",
            "reason": "Complete factorial supports a descriptive interaction pattern, not significance",
        },
        {
            "evidence": "Cell-line-stratified paired image bootstrap",
            "dataset": "LIVECell",
            "role": "official test",
            "replication": "two fixed models per condition; 2000 resamples",
            "protocol": "seeds 42/43 only",
            "paper_use": "MAIN_OR_SUPPLEMENT_WITH_SCOPE",
            "reason": "Measures test-image sampling only; does not include seed44 or training randomness",
        },
        {
            "evidence": "External detector-component transfer",
            "dataset": "Cellpose 2D",
            "role": "external held-out split",
            "replication": "2 paired conditions plus COCO-full x seeds 42/43/44",
            "protocol": "27 train images; 68 evaluation images; maxDets=100; 20 epochs",
            "paper_use": "MAIN_SECONDARY",
            "reason": "Paired component ablation plus a separate COCO-full transferable reference; not protocol-identical to LIVECell",
        },
        {
            "evidence": "Prediction inventory and qualitative overlays",
            "dataset": "LIVECell and Cellpose 2D",
            "role": "diagnostic",
            "replication": "seed44 inventory; deterministic GT-count case selection",
            "protocol": "frozen stored predictions; no performance-based case selection",
            "paper_use": "MAIN_AND_SUPPLEMENT_DIAGNOSTIC",
            "reason": "Checks the 3000-detection ceiling and displays residual dense-scene errors without changing endpoints",
        },
        {
            "evidence": "Cellpose SimCLR transfer",
            "dataset": "Cellpose 2D",
            "role": "external held-out split",
            "replication": "seed42 only; three learning-rate attempts",
            "protocol": "two non-finite runs; lr=1e-5 completed at AP 0.0030",
            "paper_use": "SUPPLEMENT_FAILURE_ANALYSIS",
            "reason": "Incomplete and uses a different learning rate; cannot enter the three-seed comparison",
        },
        {
            "evidence": "Cross-cell-line A/B/C",
            "dataset": "LIVECell",
            "role": "custom holdout validation",
            "replication": "3 methods x 2 seeds for each split",
            "protocol": "legacy maxDets=100 and per-image training cap",
            "paper_use": "SUPPLEMENT_EXPLORATORY",
            "reason": "Useful heterogeneity evidence but not directly comparable with the final dense protocol",
        },
        {
            "evidence": "LIVECell 100% and freeze ablations",
            "dataset": "LIVECell",
            "role": "development validation",
            "replication": "full-data n=2; freeze ablation n=1",
            "protocol": "legacy capped protocol",
            "paper_use": "SUPPLEMENT_EXPLORATORY",
            "reason": "Mechanistic context only; superseded for primary quantitative claims",
        },
        {
            "evidence": "CKA, effective rank, and probes",
            "dataset": "LIVECell",
            "role": "training-set diagnostic",
            "replication": "seed42",
            "protocol": "global spatial pooling before centered CKA",
            "paper_use": "SUPPLEMENT_DESCRIPTIVE",
            "reason": "Cannot establish mechanism or preserve local boundary structure",
        },
        {
            "evidence": "MS-CLACL morphology-aware runs",
            "dataset": "LIVECell",
            "role": "invalid exploration",
            "replication": "various",
            "protocol": "morphology scale used validation/test annotation statistics",
            "paper_use": "EXCLUDE_NUMERIC_RESULTS",
            "reason": "Information leakage and unequal pretraining budget invalidate quantitative comparison",
        },
        {
            "evidence": "Early default-protocol and label-fraction runs",
            "dataset": "LIVECell",
            "role": "superseded validation",
            "replication": "mostly single runs across 2%/4%/5%/25%/50%",
            "protocol": "default maxDets=100; unequal epoch/update budgets; incomplete configs",
            "paper_use": "EXCLUDE",
            "reason": "Cannot support a controlled label-fraction curve; superseded by the audited dense protocol",
        },
        {
            "evidence": "BBBC039",
            "dataset": "BBBC039",
            "role": "downloaded data only",
            "replication": "none",
            "protocol": "200 images and masks present locally; no audited training/evaluation run",
            "paper_use": "EXCLUDE",
            "reason": "A downloaded dataset is not experimental evidence",
        },
    ]
    write_csv(EVIDENCE / "evidence_registry.csv", rows)
    return rows


def write_audit(primary: list[dict], paired: list[dict], density: list[dict], cellpose: list[dict]) -> None:
    primary_by_key = {row["method_key"]: row for row in primary}
    cellpose_delta = next(row for row in cellpose if row["method"].startswith("Paired difference"))
    cellpose_coco_full = next(row for row in cellpose if row["method"] == "COCO-full detector")
    factorial_effects = read_csv(TABLES / "factorial_100epoch_effects_by_seed.csv")
    extreme = {row["method_key"]: row for row in density if row["density"] == "extreme"}
    text = f"""# MVA Evidence Audit

Date: 2026-07-23

## Primary endpoint

The official LIVECell test comparison is complete for four conditions and three training seeds. All runs used 162 official 5% training images, 20 epochs (3,240 optimizer updates), all annotated instances, and `maxDets=3000`. The primary mean mask AP values are:

- COCO-full: {float(primary_by_key['coco_full']['segm_AP_mean']):.4f} +/- {float(primary_by_key['coco_full']['segm_AP_sample_SD']):.4f}
- ImageNet+COCO: {float(primary_by_key['imagenet_coco']['segm_AP_mean']):.4f} +/- {float(primary_by_key['imagenet_coco']['segm_AP_sample_SD']):.4f}
- SimCLR+COCO: {float(primary_by_key['simclr_coco']['segm_AP_mean']):.4f} +/- {float(primary_by_key['simclr_coco']['segm_AP_sample_SD']):.4f}
- SupCon+COCO: {float(primary_by_key['supcon_coco']['segm_AP_mean']):.4f} +/- {float(primary_by_key['supcon_coco']['segm_AP_sample_SD']):.4f}

The three-seed paired table reports effect magnitudes and whether all observed signs agree. It does not provide a hypothesis test. The handoff phrase "all three seeds exclude zero" is invalid and must be replaced by "all three paired seed differences have the same sign."

## Corrected training-step record

Remote `summary.json` and `loss_log.csv` files show 3,240 updates for every seed-44 arm. The handoff counts 12,960 and 32,400 arose from multiplying updates by the number of logged loss components; they are not optimizer-step counts.

## Dense evaluation

Density-stratified AP@3000 and the four-cap `maxDets` response are available for three seeds. Extreme-density AP ranges from {min(float(row['segm_AP_mean']) for row in extreme.values()):.4f} to {max(float(row['segm_AP_mean']) for row in extreme.values()):.4f}. The seed44 cap re-evaluation used the stored formal predictions, and every AP@3000 reproduction gate passed with absolute error 0.0 before lower-cap values were accepted.

## Three-seed factorial control

The complete COCO/SimCLR x 1x/5x matrix is available for seeds 42, 43, and 44. All three seeds show COCO above SimCLR at 1x, SimCLR above COCO at 5x, a negative 5x-minus-1x response for COCO, and a positive response for SimCLR. The observed differences-in-differences are {', '.join(f"{float(row['difference_in_differences']):+.4f}" for row in factorial_effects)}. This is a repeated descriptive pattern, not a population interaction significance test.

## External Cellpose evidence

The paired external comparison contains ImageNet backbone and ImageNet+COCO components, each with three seeds. The paired ImageNet+COCO improvement is {float(cellpose_delta['segm_AP_mean']):.4f} +/- {float(cellpose_delta['segm_AP_sample_SD']):.4f}; all three observed differences are positive. A separate COCO-full transferable initialization reaches {float(cellpose_coco_full['segm_AP_mean']):.4f} +/- {float(cellpose_coco_full['segm_AP_sample_SD']):.4f}. It transfers all compatible COCO modules, reinitializes the binary predictors, and is not part of the fixed-ImageNet paired ablation. This experiment used one fixed 27-image subset and 68 evaluation images. Seven training images exceeded the 128-instance limit; retaining the largest instances excluded 783/2,886 annotations. Twenty-eight evaluation images exceeded 100 ground-truth instances, so AP@100 is a shared capped transfer comparison rather than an uncapped Cellpose benchmark or a protocol-identical LIVECell replication.

The Cellpose SimCLR evidence is incomplete: two learning-rate attempts became non-finite and one seed42 run at `1e-5` completed with AP 0.0030. It may be reported as a failure analysis in supplementary material, but it must not be pooled with the three-seed external table. No Cellpose SupCon run exists.

## Previously available but unused evidence

- Cross-cell-line A/B/C, full-data, freeze, CKA, and probe results exist and remain useful as supplementary exploratory analyses.
- BBBC039 images and masks are present, but no audited model result exists; the dataset must not be described as an evaluated benchmark.
- MS-CLACL numeric results are excluded because the morphology scale used validation/test annotation statistics and the pretraining budget was not matched.
- Early 2%/4%/5%/25%/50% results used the default detection cap, unequal budgets, and incomplete configuration records; they cannot form a controlled label-fraction curve and are superseded by the final dense protocol.

See `evidence/evidence_registry.csv` for the complete use/exclusion decision.
"""
    (EVIDENCE / "MVA_EVIDENCE_AUDIT_2026-07-23.md").write_text(text, encoding="utf-8")


def main() -> None:
    TABLES.mkdir(parents=True, exist_ok=True)
    EVIDENCE.mkdir(parents=True, exist_ok=True)
    primary, paired = build_primary_tables()
    density = build_density_n3()
    build_maxdets_n3()
    cellpose = build_cellpose_n3()
    build_cellpose_split_manifest()
    copy_control_tables()
    build_registry()
    write_audit(primary, paired, density, cellpose)
    print(f"MVA_EVIDENCE_TABLES_OK tables={TABLES} evidence={EVIDENCE}")


if __name__ == "__main__":
    main()
