__init__.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. """Pipeline task registry and scenario definitions.
  2. A *task* is a callable ``task(tracker, logger, config, state) -> None``. A
  3. *scenario* is an ordered list of task ids the pipeline runs. Tasks communicate
  4. through the mutable ``state`` dict (dataloaders, model paths, control events).
  5. Pipeline stages (see ai/ARCHITECTURE.md):
  6. 1. load_data -> implemented
  7. 2. train_regular -> implemented
  8. 3. train_bayesian -> implemented
  9. 4. evaluate_regular -> implemented (TEST split; *_val variants do VALIDATION)
  10. 5. evaluate_bayesian-> implemented (TEST split; *_val variants do VALIDATION)
  11. 6. evaluate_noisy -> implemented (TEST split; *_val variant does VALIDATION)
  12. combine_evaluations -> pools the per-split evaluations into combined_*.nc
  13. 7. run_analysis -> reads evaluations (prefers combined_*), writes analysis/.
  14. load_models -> implemented; discovers saved .pt ensembles on disk and
  15. records their paths in ``state`` so evaluation is
  16. decoupled from training (training frees models from VRAM
  17. after saving). Evaluation loads one model at a time.
  18. """
  19. import os
  20. from typing import Any, Callable, Dict, List, Tuple, TypedDict
  21. from . import analyze
  22. from . import evaluate
  23. from . import extra_info
  24. from . import load_data
  25. from . import load_models
  26. from . import train_bayesian
  27. from . import train_normal
  28. from . import kfold # imported last: depends on the sibling task modules above
  29. class TaskEntry(TypedDict):
  30. task_name: str
  31. task_func: Callable[..., None]
  32. class ScenarioEntry(TypedDict):
  33. label: str
  34. tasks: List[str]
  35. PIPELINE_TASKS: Dict[str, TaskEntry] = {
  36. "load_data": {
  37. "task_name": "Load Image and ADNIMERGE",
  38. "task_func": load_data.load_data_task,
  39. },
  40. "train_regular": {
  41. "task_name": "Train Regular Models",
  42. "task_func": train_normal.train_normal_task,
  43. },
  44. "train_bayesian": {
  45. "task_name": "Train Bayesian Models",
  46. "task_func": train_bayesian.train_bayesian_task,
  47. },
  48. "load_models": {
  49. "task_name": "Load Saved Models",
  50. "task_func": load_models.load_models_task,
  51. },
  52. "evaluate_regular": {
  53. "task_name": "Evaluate Regular Models",
  54. "task_func": evaluate.evaluate_normal_task,
  55. },
  56. "evaluate_bayesian": {
  57. "task_name": "Evaluate Bayesian Models",
  58. "task_func": evaluate.evaluate_bayesian_task,
  59. },
  60. "evaluate_noisy": {
  61. "task_name": "Evaluate Models on Noised Data",
  62. "task_func": evaluate.evaluate_noisy_task,
  63. },
  64. "evaluate_regular_val": {
  65. "task_name": "Evaluate Regular Models (Validation)",
  66. "task_func": evaluate.evaluate_normal_val_task,
  67. },
  68. "evaluate_bayesian_val": {
  69. "task_name": "Evaluate Bayesian Models (Validation)",
  70. "task_func": evaluate.evaluate_bayesian_val_task,
  71. },
  72. "evaluate_noisy_val": {
  73. "task_name": "Evaluate Models on Noised Data (Validation)",
  74. "task_func": evaluate.evaluate_noisy_val_task,
  75. },
  76. "evaluate_noisy_extra": {
  77. "task_name": "Evaluate Additional Noise Levels",
  78. "task_func": evaluate.evaluate_noisy_extra_task,
  79. },
  80. "evaluate_noisy_extra_val": {
  81. "task_name": "Evaluate Additional Noise Levels (Validation)",
  82. "task_func": evaluate.evaluate_noisy_extra_val_task,
  83. },
  84. "extra_info": {
  85. "task_name": "Generate Extra Information",
  86. "task_func": extra_info.extra_info_task,
  87. },
  88. "combine_evaluations": {
  89. "task_name": "Combine Split Evaluations",
  90. "task_func": evaluate.combine_evaluations_task,
  91. },
  92. "run_analysis": {
  93. "task_name": "Analyze Evaluations",
  94. "task_func": analyze.run_analysis_task,
  95. },
  96. "run_kfold": {
  97. "task_name": "K-Fold Cross-Validation",
  98. "task_func": kfold.run_kfold_task,
  99. },
  100. }
  101. # --------------------------------------------------------------------------
  102. # Output artifact categories and overwrite protection
  103. # --------------------------------------------------------------------------
  104. # Each category maps to the work_dir subdirectories it owns and whether it is
  105. # "protected" -- i.e. expensive/slow to regenerate, so overwriting it warrants a
  106. # confirmation prompt. Analysis artifacts are cheap to regenerate and therefore
  107. # unprotected: reruns of an analysis-only scenario proceed without a warning.
  108. class ArtifactCategory(TypedDict):
  109. dirs: Tuple[str, ...]
  110. protected: bool
  111. desc: str
  112. ARTIFACT_CATEGORIES: Dict[str, ArtifactCategory] = {
  113. "models": {
  114. "dirs": ("normal_models", "bayesian_models"),
  115. "protected": True,
  116. "desc": "trained model checkpoints",
  117. },
  118. "evaluations": {
  119. "dirs": ("evaluations",),
  120. "protected": True,
  121. "desc": "model evaluation outputs (netCDF)",
  122. },
  123. "kfold": {
  124. "dirs": ("folds",),
  125. "protected": True,
  126. "desc": "per-fold k-fold models & evaluations",
  127. },
  128. "analysis": {
  129. "dirs": (analyze.ANALYSIS_SUBDIR,),
  130. "protected": False,
  131. "desc": "analysis artifacts (regeneratable)",
  132. },
  133. }
  134. # Which artifact category each task writes to disk. Tasks not listed here (e.g.
  135. # load_data, load_models) produce no persistent, overwriteable output.
  136. TASK_WRITES = {
  137. "train_regular": ("models",),
  138. "train_bayesian": ("models",),
  139. "evaluate_regular": ("evaluations",),
  140. "evaluate_bayesian": ("evaluations",),
  141. "evaluate_noisy": ("evaluations",),
  142. "evaluate_regular_val": ("evaluations",),
  143. "evaluate_bayesian_val": ("evaluations",),
  144. "evaluate_noisy_val": ("evaluations",),
  145. "evaluate_noisy_extra": ("evaluations",),
  146. "evaluate_noisy_extra_val": ("evaluations",),
  147. "extra_info": ("analysis",),
  148. "combine_evaluations": ("evaluations",),
  149. "run_analysis": ("analysis",),
  150. # k-fold writes per-fold models+evals under folds/ and the aggregated OOF
  151. # files under evaluations/; both are protected (slow to regenerate).
  152. "run_kfold": ("kfold", "evaluations"),
  153. }
  154. def scenario_output_categories(scenario_id: str) -> List[str]:
  155. """Return the artifact categories a scenario writes, in first-seen order."""
  156. categories: List[str] = []
  157. for task_id in SCENARIOS[scenario_id]["tasks"]:
  158. for category in TASK_WRITES.get(task_id, ()):
  159. if category not in categories:
  160. categories.append(category)
  161. return categories
  162. def protected_output_conflicts(
  163. work_dir: str, scenario_id: str
  164. ) -> List[Tuple[str, str]]:
  165. """Return ``(category, subdir)`` for PROTECTED outputs the scenario writes
  166. that already exist and are non-empty. Empty result => safe to run without a
  167. destructive-overwrite prompt (only unprotected/regeneratable outputs, if any,
  168. would be replaced)."""
  169. conflicts: List[Tuple[str, str]] = []
  170. for category in scenario_output_categories(scenario_id):
  171. meta = ARTIFACT_CATEGORIES.get(category)
  172. if not meta or not meta["protected"]:
  173. continue
  174. for subdir in meta["dirs"]:
  175. path = os.path.join(work_dir, subdir)
  176. if os.path.isdir(path) and os.listdir(path):
  177. conflicts.append((category, subdir))
  178. return conflicts
  179. def describe_protected_conflicts(work_dir: str, scenario_id: str) -> List[str]:
  180. """Human-readable one-liners for each protected overwrite conflict."""
  181. lines: List[str] = []
  182. for category, subdir in protected_output_conflicts(work_dir, scenario_id):
  183. desc = ARTIFACT_CATEGORIES[category]["desc"]
  184. lines.append(f"{subdir}/ — {desc}")
  185. return lines
  186. # Evaluation of both held-out splits (test + val), pooled for analysis. Shared by
  187. # the train and load scenarios so they stay in lockstep.
  188. _EVAL_ALL_SPLITS = [
  189. "evaluate_regular",
  190. "evaluate_bayesian",
  191. "evaluate_regular_val",
  192. "evaluate_bayesian_val",
  193. ]
  194. _EVAL_ALL_SPLITS_NOISY = ["evaluate_noisy", "evaluate_noisy_val"]
  195. SCENARIOS: Dict[str, ScenarioEntry] = {
  196. "scen_train_all": {
  197. "label": "1. Train, Evaluate (test+val), Noise, & Analyze",
  198. "tasks": [
  199. "load_data",
  200. "train_regular",
  201. "train_bayesian",
  202. "load_models",
  203. *_EVAL_ALL_SPLITS,
  204. *_EVAL_ALL_SPLITS_NOISY,
  205. "combine_evaluations",
  206. "extra_info",
  207. "run_analysis",
  208. ],
  209. },
  210. "scen_load_all": {
  211. "label": "2. Load, Evaluate (test+val), Noise, & Analyze",
  212. "tasks": [
  213. "load_data",
  214. "load_models",
  215. *_EVAL_ALL_SPLITS,
  216. *_EVAL_ALL_SPLITS_NOISY,
  217. "combine_evaluations",
  218. "extra_info",
  219. "run_analysis",
  220. ],
  221. },
  222. "scen_load_eval": {
  223. "label": "3. Load & Evaluate (test+val, Skip Noise) & Analyze",
  224. "tasks": [
  225. "load_data",
  226. "load_models",
  227. *_EVAL_ALL_SPLITS,
  228. "combine_evaluations",
  229. "extra_info",
  230. "run_analysis",
  231. ],
  232. },
  233. # Extend an existing noise sweep: computes ONLY the extra sigmas, merges them
  234. # onto the precomputed sweep, and re-analyses. No retraining, no recompute of
  235. # the noise levels already on disk.
  236. "scen_extend_noise": {
  237. "label": "6. Extend Noise Sweep (extra sigmas + merge + analyze)",
  238. "tasks": [
  239. "load_data",
  240. "load_models",
  241. "evaluate_noisy_extra",
  242. "evaluate_noisy_extra_val",
  243. "combine_evaluations",
  244. "extra_info",
  245. "run_analysis",
  246. ],
  247. },
  248. # Re-run the analysis over evaluations already on disk. No GPU work: it only
  249. # re-merges the per-split / base+extra evaluation files, regenerates the
  250. # extra-info figures (which need the dataset, hence load_data), and redraws
  251. # every analysis. Use after changing an analysis parameter.
  252. "scen_reanalyze": {
  253. "label": "7. Re-analyze Existing Evaluations (merge + extras + analysis)",
  254. "tasks": [
  255. "load_data",
  256. "combine_evaluations",
  257. "extra_info",
  258. "run_analysis",
  259. ],
  260. },
  261. "scen_analyze": {
  262. "label": "4. Analyze Existing Evaluations",
  263. "tasks": [
  264. "run_analysis",
  265. ],
  266. },
  267. # Set aside for now (kept working, not part of the default flow): full
  268. # patient-grouped cross-validation. Costs ~k x a normal train+eval run.
  269. "scen_kfold": {
  270. "label": "5. K-Fold Cross-Validation (train + OOF eval + analyze)",
  271. "tasks": [
  272. "run_kfold",
  273. "run_analysis",
  274. ],
  275. },
  276. }