__init__.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  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 load_data
  24. from . import load_models
  25. from . import train_bayesian
  26. from . import train_normal
  27. from . import kfold # imported last: depends on the sibling task modules above
  28. class TaskEntry(TypedDict):
  29. task_name: str
  30. task_func: Callable[..., None]
  31. class ScenarioEntry(TypedDict):
  32. label: str
  33. tasks: List[str]
  34. PIPELINE_TASKS: Dict[str, TaskEntry] = {
  35. "load_data": {
  36. "task_name": "Load Image and ADNIMERGE",
  37. "task_func": load_data.load_data_task,
  38. },
  39. "train_regular": {
  40. "task_name": "Train Regular Models",
  41. "task_func": train_normal.train_normal_task,
  42. },
  43. "train_bayesian": {
  44. "task_name": "Train Bayesian Models",
  45. "task_func": train_bayesian.train_bayesian_task,
  46. },
  47. "load_models": {
  48. "task_name": "Load Saved Models",
  49. "task_func": load_models.load_models_task,
  50. },
  51. "evaluate_regular": {
  52. "task_name": "Evaluate Regular Models",
  53. "task_func": evaluate.evaluate_normal_task,
  54. },
  55. "evaluate_bayesian": {
  56. "task_name": "Evaluate Bayesian Models",
  57. "task_func": evaluate.evaluate_bayesian_task,
  58. },
  59. "evaluate_noisy": {
  60. "task_name": "Evaluate Models on Noised Data",
  61. "task_func": evaluate.evaluate_noisy_task,
  62. },
  63. "evaluate_regular_val": {
  64. "task_name": "Evaluate Regular Models (Validation)",
  65. "task_func": evaluate.evaluate_normal_val_task,
  66. },
  67. "evaluate_bayesian_val": {
  68. "task_name": "Evaluate Bayesian Models (Validation)",
  69. "task_func": evaluate.evaluate_bayesian_val_task,
  70. },
  71. "evaluate_noisy_val": {
  72. "task_name": "Evaluate Models on Noised Data (Validation)",
  73. "task_func": evaluate.evaluate_noisy_val_task,
  74. },
  75. "combine_evaluations": {
  76. "task_name": "Combine Split Evaluations",
  77. "task_func": evaluate.combine_evaluations_task,
  78. },
  79. "run_analysis": {
  80. "task_name": "Analyze Evaluations",
  81. "task_func": analyze.run_analysis_task,
  82. },
  83. "run_kfold": {
  84. "task_name": "K-Fold Cross-Validation",
  85. "task_func": kfold.run_kfold_task,
  86. },
  87. }
  88. # --------------------------------------------------------------------------
  89. # Output artifact categories and overwrite protection
  90. # --------------------------------------------------------------------------
  91. # Each category maps to the work_dir subdirectories it owns and whether it is
  92. # "protected" -- i.e. expensive/slow to regenerate, so overwriting it warrants a
  93. # confirmation prompt. Analysis artifacts are cheap to regenerate and therefore
  94. # unprotected: reruns of an analysis-only scenario proceed without a warning.
  95. class ArtifactCategory(TypedDict):
  96. dirs: Tuple[str, ...]
  97. protected: bool
  98. desc: str
  99. ARTIFACT_CATEGORIES: Dict[str, ArtifactCategory] = {
  100. "models": {
  101. "dirs": ("normal_models", "bayesian_models"),
  102. "protected": True,
  103. "desc": "trained model checkpoints",
  104. },
  105. "evaluations": {
  106. "dirs": ("evaluations",),
  107. "protected": True,
  108. "desc": "model evaluation outputs (netCDF)",
  109. },
  110. "kfold": {
  111. "dirs": ("folds",),
  112. "protected": True,
  113. "desc": "per-fold k-fold models & evaluations",
  114. },
  115. "analysis": {
  116. "dirs": (analyze.ANALYSIS_SUBDIR,),
  117. "protected": False,
  118. "desc": "analysis artifacts (regeneratable)",
  119. },
  120. }
  121. # Which artifact category each task writes to disk. Tasks not listed here (e.g.
  122. # load_data, load_models) produce no persistent, overwriteable output.
  123. TASK_WRITES = {
  124. "train_regular": ("models",),
  125. "train_bayesian": ("models",),
  126. "evaluate_regular": ("evaluations",),
  127. "evaluate_bayesian": ("evaluations",),
  128. "evaluate_noisy": ("evaluations",),
  129. "evaluate_regular_val": ("evaluations",),
  130. "evaluate_bayesian_val": ("evaluations",),
  131. "evaluate_noisy_val": ("evaluations",),
  132. "combine_evaluations": ("evaluations",),
  133. "run_analysis": ("analysis",),
  134. # k-fold writes per-fold models+evals under folds/ and the aggregated OOF
  135. # files under evaluations/; both are protected (slow to regenerate).
  136. "run_kfold": ("kfold", "evaluations"),
  137. }
  138. def scenario_output_categories(scenario_id: str) -> List[str]:
  139. """Return the artifact categories a scenario writes, in first-seen order."""
  140. categories: List[str] = []
  141. for task_id in SCENARIOS[scenario_id]["tasks"]:
  142. for category in TASK_WRITES.get(task_id, ()):
  143. if category not in categories:
  144. categories.append(category)
  145. return categories
  146. def protected_output_conflicts(
  147. work_dir: str, scenario_id: str
  148. ) -> List[Tuple[str, str]]:
  149. """Return ``(category, subdir)`` for PROTECTED outputs the scenario writes
  150. that already exist and are non-empty. Empty result => safe to run without a
  151. destructive-overwrite prompt (only unprotected/regeneratable outputs, if any,
  152. would be replaced)."""
  153. conflicts: List[Tuple[str, str]] = []
  154. for category in scenario_output_categories(scenario_id):
  155. meta = ARTIFACT_CATEGORIES.get(category)
  156. if not meta or not meta["protected"]:
  157. continue
  158. for subdir in meta["dirs"]:
  159. path = os.path.join(work_dir, subdir)
  160. if os.path.isdir(path) and os.listdir(path):
  161. conflicts.append((category, subdir))
  162. return conflicts
  163. def describe_protected_conflicts(work_dir: str, scenario_id: str) -> List[str]:
  164. """Human-readable one-liners for each protected overwrite conflict."""
  165. lines: List[str] = []
  166. for category, subdir in protected_output_conflicts(work_dir, scenario_id):
  167. desc = ARTIFACT_CATEGORIES[category]["desc"]
  168. lines.append(f"{subdir}/ — {desc}")
  169. return lines
  170. # Evaluation of both held-out splits (test + val), pooled for analysis. Shared by
  171. # the train and load scenarios so they stay in lockstep.
  172. _EVAL_ALL_SPLITS = [
  173. "evaluate_regular",
  174. "evaluate_bayesian",
  175. "evaluate_regular_val",
  176. "evaluate_bayesian_val",
  177. ]
  178. _EVAL_ALL_SPLITS_NOISY = ["evaluate_noisy", "evaluate_noisy_val"]
  179. SCENARIOS: Dict[str, ScenarioEntry] = {
  180. "scen_train_all": {
  181. "label": "1. Train, Evaluate (test+val), Noise, & Analyze",
  182. "tasks": [
  183. "load_data",
  184. "train_regular",
  185. "train_bayesian",
  186. "load_models",
  187. *_EVAL_ALL_SPLITS,
  188. *_EVAL_ALL_SPLITS_NOISY,
  189. "combine_evaluations",
  190. "run_analysis",
  191. ],
  192. },
  193. "scen_load_all": {
  194. "label": "2. Load, Evaluate (test+val), Noise, & Analyze",
  195. "tasks": [
  196. "load_data",
  197. "load_models",
  198. *_EVAL_ALL_SPLITS,
  199. *_EVAL_ALL_SPLITS_NOISY,
  200. "combine_evaluations",
  201. "run_analysis",
  202. ],
  203. },
  204. "scen_load_eval": {
  205. "label": "3. Load & Evaluate (test+val, Skip Noise) & Analyze",
  206. "tasks": [
  207. "load_data",
  208. "load_models",
  209. *_EVAL_ALL_SPLITS,
  210. "combine_evaluations",
  211. "run_analysis",
  212. ],
  213. },
  214. "scen_analyze": {
  215. "label": "4. Analyze Existing Evaluations",
  216. "tasks": [
  217. "run_analysis",
  218. ],
  219. },
  220. # Set aside for now (kept working, not part of the default flow): full
  221. # patient-grouped cross-validation. Costs ~k x a normal train+eval run.
  222. "scen_kfold": {
  223. "label": "5. K-Fold Cross-Validation (train + OOF eval + analyze)",
  224. "tasks": [
  225. "run_kfold",
  226. "run_analysis",
  227. ],
  228. },
  229. }