__init__.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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
  10. 5. evaluate_bayesian-> implemented
  11. 6. evaluate_noisy -> implemented
  12. 7. run_analysis -> skeleton (step 7); reads evaluations, writes analysis/.
  13. load_models -> implemented; discovers saved .pt ensembles on disk and
  14. records their paths in ``state`` so evaluation is
  15. decoupled from training (training frees models from VRAM
  16. after saving). Evaluation loads one model at a time.
  17. """
  18. import os
  19. from typing import Any, Callable, Dict, List, Tuple, TypedDict
  20. from . import analysis
  21. from . import evaluate
  22. from . import load_data
  23. from . import load_models
  24. from . import train_bayesian
  25. from . import train_normal
  26. from . import kfold # imported last: depends on the sibling task modules above
  27. class TaskEntry(TypedDict):
  28. task_name: str
  29. task_func: Callable[..., None]
  30. class ScenarioEntry(TypedDict):
  31. label: str
  32. tasks: List[str]
  33. PIPELINE_TASKS: Dict[str, TaskEntry] = {
  34. "load_data": {
  35. "task_name": "Load Image and ADNIMERGE",
  36. "task_func": load_data.load_data_task,
  37. },
  38. "train_regular": {
  39. "task_name": "Train Regular Models",
  40. "task_func": train_normal.train_normal_task,
  41. },
  42. "train_bayesian": {
  43. "task_name": "Train Bayesian Models",
  44. "task_func": train_bayesian.train_bayesian_task,
  45. },
  46. "load_models": {
  47. "task_name": "Load Saved Models",
  48. "task_func": load_models.load_models_task,
  49. },
  50. "evaluate_regular": {
  51. "task_name": "Evaluate Regular Models",
  52. "task_func": evaluate.evaluate_normal_task,
  53. },
  54. "evaluate_bayesian": {
  55. "task_name": "Evaluate Bayesian Models",
  56. "task_func": evaluate.evaluate_bayesian_task,
  57. },
  58. "evaluate_noisy": {
  59. "task_name": "Evaluate Models on Noised Data",
  60. "task_func": evaluate.evaluate_noisy_task,
  61. },
  62. "run_analysis": {
  63. "task_name": "Analyze Evaluations",
  64. "task_func": analysis.run_analysis_task,
  65. },
  66. "run_kfold": {
  67. "task_name": "K-Fold Cross-Validation",
  68. "task_func": kfold.run_kfold_task,
  69. },
  70. }
  71. # --------------------------------------------------------------------------
  72. # Output artifact categories and overwrite protection
  73. # --------------------------------------------------------------------------
  74. # Each category maps to the work_dir subdirectories it owns and whether it is
  75. # "protected" -- i.e. expensive/slow to regenerate, so overwriting it warrants a
  76. # confirmation prompt. Analysis artifacts are cheap to regenerate and therefore
  77. # unprotected: reruns of an analysis-only scenario proceed without a warning.
  78. class ArtifactCategory(TypedDict):
  79. dirs: Tuple[str, ...]
  80. protected: bool
  81. desc: str
  82. ARTIFACT_CATEGORIES: Dict[str, ArtifactCategory] = {
  83. "models": {
  84. "dirs": ("normal_models", "bayesian_models"),
  85. "protected": True,
  86. "desc": "trained model checkpoints",
  87. },
  88. "evaluations": {
  89. "dirs": ("evaluations",),
  90. "protected": True,
  91. "desc": "model evaluation outputs (netCDF)",
  92. },
  93. "kfold": {
  94. "dirs": ("folds",),
  95. "protected": True,
  96. "desc": "per-fold k-fold models & evaluations",
  97. },
  98. "analysis": {
  99. "dirs": (analysis.ANALYSIS_SUBDIR,),
  100. "protected": False,
  101. "desc": "analysis artifacts (regeneratable)",
  102. },
  103. }
  104. # Which artifact category each task writes to disk. Tasks not listed here (e.g.
  105. # load_data, load_models) produce no persistent, overwriteable output.
  106. TASK_WRITES = {
  107. "train_regular": ("models",),
  108. "train_bayesian": ("models",),
  109. "evaluate_regular": ("evaluations",),
  110. "evaluate_bayesian": ("evaluations",),
  111. "evaluate_noisy": ("evaluations",),
  112. "run_analysis": ("analysis",),
  113. # k-fold writes per-fold models+evals under folds/ and the aggregated OOF
  114. # files under evaluations/; both are protected (slow to regenerate).
  115. "run_kfold": ("kfold", "evaluations"),
  116. }
  117. def scenario_output_categories(scenario_id: str) -> List[str]:
  118. """Return the artifact categories a scenario writes, in first-seen order."""
  119. categories: List[str] = []
  120. for task_id in SCENARIOS[scenario_id]["tasks"]:
  121. for category in TASK_WRITES.get(task_id, ()):
  122. if category not in categories:
  123. categories.append(category)
  124. return categories
  125. def protected_output_conflicts(
  126. work_dir: str, scenario_id: str
  127. ) -> List[Tuple[str, str]]:
  128. """Return ``(category, subdir)`` for PROTECTED outputs the scenario writes
  129. that already exist and are non-empty. Empty result => safe to run without a
  130. destructive-overwrite prompt (only unprotected/regeneratable outputs, if any,
  131. would be replaced)."""
  132. conflicts: List[Tuple[str, str]] = []
  133. for category in scenario_output_categories(scenario_id):
  134. meta = ARTIFACT_CATEGORIES.get(category)
  135. if not meta or not meta["protected"]:
  136. continue
  137. for subdir in meta["dirs"]:
  138. path = os.path.join(work_dir, subdir)
  139. if os.path.isdir(path) and os.listdir(path):
  140. conflicts.append((category, subdir))
  141. return conflicts
  142. def describe_protected_conflicts(work_dir: str, scenario_id: str) -> List[str]:
  143. """Human-readable one-liners for each protected overwrite conflict."""
  144. lines: List[str] = []
  145. for category, subdir in protected_output_conflicts(work_dir, scenario_id):
  146. desc = ARTIFACT_CATEGORIES[category]["desc"]
  147. lines.append(f"{subdir}/ — {desc}")
  148. return lines
  149. SCENARIOS: Dict[str, ScenarioEntry] = {
  150. "scen_train_all": {
  151. "label": "1. Train, Evaluate, & Noise Analysis",
  152. "tasks": [
  153. "load_data",
  154. "train_regular",
  155. "train_bayesian",
  156. "load_models",
  157. "evaluate_regular",
  158. "evaluate_bayesian",
  159. "evaluate_noisy",
  160. ],
  161. },
  162. "scen_load_all": {
  163. "label": "2. Load, Evaluate, & Noise Analysis",
  164. "tasks": [
  165. "load_data",
  166. "load_models",
  167. "evaluate_regular",
  168. "evaluate_bayesian",
  169. "evaluate_noisy",
  170. ],
  171. },
  172. "scen_load_eval": {
  173. "label": "3. Load & Evaluate (Skip Noise)",
  174. "tasks": [
  175. "load_data",
  176. "load_models",
  177. "evaluate_regular",
  178. "evaluate_bayesian",
  179. ],
  180. },
  181. "scen_analyze": {
  182. "label": "4. Analyze Existing Evaluations",
  183. "tasks": [
  184. "run_analysis",
  185. ],
  186. },
  187. "scen_kfold": {
  188. "label": "5. K-Fold Cross-Validation (train + OOF eval + analyze)",
  189. "tasks": [
  190. "run_kfold",
  191. "run_analysis",
  192. ],
  193. },
  194. }