Files
hypertower/scripts/testing/test_v2_papila_loaders.py
T
2026-02-24 10:39:48 +01:00

136 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Describe PAPILA splits and V2 slot-based loaders."""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from types import SimpleNamespace
import torch
# Ensure repo root is importable when running as: python3 scripts/test_v2_papila_loaders.py
REPO_ROOT = Path(__file__).resolve().parents[1]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from classes import build_papila_clinical
from classes.v2 import build_papila_profile, PatientFirstSplitManager, SlotLoaderFactory
def _describe_split(name: str, df, label_col: str) -> str:
if df is None or df.empty:
return f"{name}: empty"
patient_ids = set(df["Patient ID"].tolist())
class_counts = dict(df.groupby(label_col).size().to_dict())
return (
f"{name}: patients={len(patient_ids)} rows={len(df)} "
f"class_rows={class_counts}"
)
def _describe_batch(batch: dict) -> list[str]:
lines = []
for key, val in batch.items():
if isinstance(val, torch.Tensor):
lines.append(f"{key}: tensor shape={tuple(val.shape)} dtype={val.dtype}")
elif isinstance(val, list):
non_none = next((v for v in val if v is not None), None)
lines.append(
f"{key}: list len={len(val)} sample_type={type(non_none).__name__ if non_none is not None else 'None'}"
)
else:
lines.append(f"{key}: {type(val).__name__}")
return lines
def main() -> None:
ap = argparse.ArgumentParser(description="Describe PAPILA splits + V2 slot-based loaders.")
ap.add_argument("--image-dir", default="Papila/FundusImages")
ap.add_argument("--clinical-dir", default="Papila/ClinicalData")
ap.add_argument("--label-col", default="Diagnosis")
ap.add_argument(
"--cat-cols",
nargs="*",
default=["Gender", "Phakic/Pseudophakic"],
help="Categorical columns for PAPILA builder.",
)
ap.add_argument("--n-splits", type=int, default=5)
ap.add_argument("--fold-seed", type=int, default=42)
ap.add_argument("--holdout-per-class", type=int, default=1)
ap.add_argument("--holdout-seed", type=int, default=123)
ap.add_argument("--batch-size", type=int, default=4)
ap.add_argument("--num-workers", type=int, default=0)
ap.add_argument("--fold", type=int, default=0, help="Which fold to inspect in detail.")
ap.add_argument(
"--sample-mode",
choices=["patient", "eye"],
default="patient",
help="Build samples per patient (multi-slot) or per eye (row-level).",
)
args = ap.parse_args()
clinical = build_papila_clinical(
image_dir=args.image_dir,
clinical_dir=args.clinical_dir,
label_col=args.label_col,
cat_cols=args.cat_cols,
n_splits=args.n_splits,
random_seed=args.fold_seed,
)
profile = build_papila_profile(
patient_col="Patient ID",
label_col=args.label_col,
sample_mode=args.sample_mode,
)
print("=== PAPILA profile slots ===")
for key, desc in profile.slot_descriptors().items():
print(f"{key}: kind={desc.kind} required={desc.required} desc={desc.description}")
print("aliases:", profile.semantic_aliases())
split_args = SimpleNamespace(
eval_mode="multiclass",
holdout_per_class=args.holdout_per_class,
holdout_seed=args.holdout_seed,
n_splits=args.n_splits,
fold_seed=args.fold_seed,
)
split_manager = PatientFirstSplitManager(patient_col="Patient ID", label_col=args.label_col)
plans = split_manager.build_plans(clinical=clinical, args=split_args, profile=profile)
print("\n=== Split summaries ===")
for i, split in enumerate(plans):
print(f"fold {i}:")
print(" " + _describe_split("train", split.train, args.label_col))
print(" " + _describe_split("val", split.val, args.label_col))
print(" " + _describe_split("holdout", split.holdout, args.label_col))
if args.fold < 0 or args.fold >= len(plans):
raise SystemExit(f"Requested fold {args.fold} but only {len(plans)} folds are available")
split = plans[args.fold]
loader_factory = SlotLoaderFactory(num_workers=args.num_workers)
loaders = loader_factory.build(
clinical=clinical,
split=split,
args=SimpleNamespace(batch_size=args.batch_size),
fold=args.fold,
profile=profile,
)
print(f"\n=== Loader inspection (fold {args.fold}) ===")
for name, loader in (("train", loaders.train), ("val", loaders.val), ("holdout", loaders.holdout)):
if loader is None:
print(f"{name}: None")
continue
print(f"{name}: batches={len(loader)} batch_size={loader.batch_size}")
batch = next(iter(loader))
for line in _describe_batch(batch):
print(f" {line}")
if __name__ == "__main__":
main()