| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- # pyright: basic
- from __future__ import annotations
- import argparse
- from pathlib import Path
- from typing import Any
- import numpy as np
- import pandas as pd
- from analysis.analysis_modules import (
- run_calibration,
- run_longitudinal,
- run_performance,
- run_physician,
- )
- from analysis.data_access import load_backend_evaluation, load_clinical_table
- from analysis.holdout_evaluation import ensure_backend_netcdf
- from analysis.noise_analysis import run_noise_analysis
- from analysis.runtime import backend_dir, init_runtime_paths, load_config, write_json
- def _parse_args() -> argparse.Namespace:
- parser = argparse.ArgumentParser(
- description=(
- "Run modular evaluation analyses for ensemble and bayesian models. "
- "All outputs are written to alnn_rewrite/analysis_output."
- )
- )
- parser.add_argument(
- "--backend",
- nargs="+",
- choices=["ensemble", "bayesian"],
- default=["ensemble", "bayesian"],
- help="Backends to evaluate.",
- )
- parser.add_argument(
- "--run-name",
- default=None,
- help="Optional run directory name under analysis_output.",
- )
- parser.add_argument(
- "--threshold-start",
- type=float,
- default=0.5,
- help="Threshold sweep start.",
- )
- parser.add_argument(
- "--threshold-stop",
- type=float,
- default=0.95,
- help="Threshold sweep stop (inclusive).",
- )
- parser.add_argument(
- "--threshold-step",
- type=float,
- default=0.05,
- help="Threshold sweep step.",
- )
- parser.add_argument(
- "--decision-threshold",
- type=float,
- default=0.5,
- help="Decision threshold used in noise analysis.",
- )
- parser.add_argument(
- "--positive-class-index",
- type=int,
- default=0,
- help="Positive class index in both predictions.img_class and labels.label.",
- )
- parser.add_argument(
- "--calibration-bins",
- type=int,
- default=10,
- help="Number of reliability bins used for ECE/MCE.",
- )
- parser.add_argument(
- "--skip-noise",
- action="store_true",
- help="Skip Gaussian noise sensitivity analysis.",
- )
- parser.add_argument(
- "--noise-sigmas",
- nargs="+",
- type=float,
- default=[
- 0.0,
- 0.01,
- 0.03,
- 0.05,
- 0.1,
- 0.2,
- 0.3,
- 0.4,
- 0.5,
- 0.6,
- 0.75,
- 1.0,
- ],
- help="Gaussian noise sigmas for sensitivity analysis.",
- )
- parser.add_argument(
- "--bayesian-mc-passes",
- type=int,
- default=20,
- help="MC forward passes for bayesian noise analysis.",
- )
- return parser.parse_args()
- def _threshold_array(start: float, stop: float, step: float) -> np.ndarray:
- if step <= 0:
- raise ValueError("threshold-step must be > 0")
- if stop < start:
- raise ValueError("threshold-stop must be >= threshold-start")
- # Include stop when it lands on a step boundary.
- n = int(round((stop - start) / step))
- return np.array([start + i * step for i in range(n + 1)], dtype=float)
- def _run_backend(
- config: dict[str, Any],
- root_dir: Path,
- backend: str,
- clinical_df: pd.DataFrame,
- args: argparse.Namespace,
- out_dir: Path,
- ) -> dict[str, Any]:
- netcdf_path = ensure_backend_netcdf(
- config=config,
- root_dir=root_dir,
- backend=backend,
- bayesian_mc_passes=int(args.bayesian_mc_passes),
- )
- evaluation = load_backend_evaluation(
- config=config,
- backend=backend,
- class_index=int(args.positive_class_index),
- )
- thresholds = _threshold_array(
- start=float(args.threshold_start),
- stop=float(args.threshold_stop),
- step=float(args.threshold_step),
- )
- summary: dict[str, Any] = {
- "backend": backend,
- "netcdf": str(netcdf_path),
- "source_file": str(evaluation.source_file),
- "uncertainty_metric": evaluation.uncertainty_metric,
- }
- summary["performance"] = run_performance(
- evaluation=evaluation,
- output_dir=out_dir,
- thresholds=thresholds,
- )
- summary["calibration"] = run_calibration(
- evaluation=evaluation,
- output_dir=out_dir,
- bins=int(args.calibration_bins),
- )
- summary["physician"] = run_physician(
- evaluation=evaluation,
- clinical_df=clinical_df,
- output_dir=out_dir,
- )
- summary["longitudinal"] = run_longitudinal(
- evaluation=evaluation,
- clinical_df=clinical_df,
- output_dir=out_dir,
- )
- if args.skip_noise:
- summary["noise"] = {"skipped": True, "reason": "--skip-noise supplied"}
- else:
- try:
- summary["noise"] = run_noise_analysis(
- config=config,
- root_dir=Path(__file__).resolve().parents[1],
- backend=backend,
- output_dir=out_dir,
- class_index=int(args.positive_class_index),
- noise_sigmas=[float(x) for x in args.noise_sigmas],
- threshold=float(args.decision_threshold),
- calibration_bins=int(args.calibration_bins),
- bayesian_mc_passes=int(args.bayesian_mc_passes),
- )
- except Exception as exc:
- summary["noise"] = {
- "skipped": True,
- "reason": f"Noise analysis failed: {exc}",
- }
- write_json(out_dir / "backend_summary.json", summary)
- return summary
- def main() -> None:
- args = _parse_args()
- analysis_dir = Path(__file__).resolve().parent
- paths = init_runtime_paths(analysis_dir=analysis_dir, run_name=args.run_name)
- config = load_config(paths.root_dir)
- clinical_df = load_clinical_table(config=config, root_dir=paths.root_dir)
- manifest: dict[str, Any] = {
- "run_dir": str(paths.run_dir),
- "output_root": str(paths.output_root),
- "positive_class_index": int(args.positive_class_index),
- "threshold_sweep": {
- "start": float(args.threshold_start),
- "stop": float(args.threshold_stop),
- "step": float(args.threshold_step),
- },
- "calibration_bins": int(args.calibration_bins),
- "noise_sigmas": [float(x) for x in args.noise_sigmas],
- "backends": {},
- }
- for backend in args.backend:
- out_dir = backend_dir(paths, backend)
- manifest["backends"][backend] = _run_backend(
- config=config,
- root_dir=paths.root_dir,
- backend=backend,
- clinical_df=clinical_df,
- args=args,
- out_dir=out_dir,
- )
- write_json(paths.run_dir / "run_manifest.json", manifest)
- print(f"Analysis complete. Results saved to {paths.run_dir}")
- if __name__ == "__main__":
- main()
|