sources.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. """Choosing which evaluation file represents which model family.
  2. A single work_dir holds several evaluation files describing the *same* ensembles:
  3. ``normal.nc`` (test), ``val_normal.nc``, ``combined_normal.nc`` (pooled),
  4. ``noisy_normal.nc`` (noise sweep), ``oof_normal.nc`` (k-fold). Analyses must pick
  5. exactly one per family per purpose, or a family would be counted twice and the
  6. numbers would silently disagree between analyses.
  7. Two selections exist:
  8. * **clean** -- the largest available sample set at the baseline noise level,
  9. used for accuracy/coverage style analyses.
  10. * **noise** -- the noise sweep (more than one noise level), used by the
  11. noise-response analyses.
  12. """
  13. from typing import Dict, Iterable, List, Tuple
  14. import numpy as np
  15. import xarray as xr
  16. from evaluation import MODEL_KIND_BAYESIAN, MODEL_KIND_NORMAL
  17. #: Canonical display order for model families.
  18. FAMILY_ORDER: List[str] = [MODEL_KIND_NORMAL, MODEL_KIND_BAYESIAN]
  19. #: Human-readable family labels.
  20. FAMILY_LABELS: Dict[str, str] = {
  21. MODEL_KIND_NORMAL: "Normal (deterministic)",
  22. MODEL_KIND_BAYESIAN: "Bayesian (MC)",
  23. }
  24. def ordered_families(kinds: Iterable[str]) -> List[str]:
  25. """Sort family kinds into the canonical Normal-then-Bayesian order."""
  26. order = {kind: i for i, kind in enumerate(FAMILY_ORDER)}
  27. return sorted(kinds, key=lambda k: order.get(k, len(order)))
  28. def baseline_noise_index(ds: xr.Dataset) -> int:
  29. """Index of the noise level closest to the clean baseline (sigma = 0)."""
  30. levels = np.asarray(ds["noise_level"].values, dtype=float)
  31. return int(np.abs(levels).argmin())
  32. def has_noise_sweep(ds: xr.Dataset) -> bool:
  33. """True when the dataset spans more than one noise level."""
  34. return int(ds.sizes.get("noise_level", 1)) > 1
  35. def noise_axis_label(ds: xr.Dataset) -> str:
  36. """Axis label for the noise level, reflecting how sigma was applied.
  37. Relative noise (the default) scales sigma by each image's own intensity
  38. standard deviation, so the units differ from absolute noise and the axis must
  39. say which it is.
  40. """
  41. if int(ds.attrs.get("noise_relative", 0)):
  42. return "Noise σ (× image std)"
  43. return "Noise σ (raw voxel units)"
  44. def _coverage_rank(stem: str, ds: xr.Dataset) -> int:
  45. """Rank a file by how many evaluation samples it covers (lower is better)."""
  46. split = str(ds.attrs.get("split", ""))
  47. if stem.startswith(("combined_", "oof_")) or "+" in split:
  48. return 0 # pooled: test+val or k-fold out-of-fold -- the most samples
  49. if split == "test":
  50. return 1
  51. if split == "val":
  52. return 2
  53. return 3 # unlabeled (files written before the split attribute existed)
  54. def source_rank(stem: str, ds: xr.Dataset) -> Tuple[int, int, str]:
  55. """Sort key for the *clean* selection: clean before noisy, then by coverage."""
  56. return (1 if "noisy" in stem else 0, _coverage_rank(stem, ds), stem)
  57. def noise_source_rank(stem: str, ds: xr.Dataset) -> Tuple[int, str]:
  58. """Sort key for the *noise* selection: widest coverage first."""
  59. return (_coverage_rank(stem, ds), stem)
  60. def _families_in(ds: xr.Dataset) -> List[str]:
  61. if "model_kind" not in ds.coords:
  62. return []
  63. return sorted({str(k) for k in np.atleast_1d(ds["model_kind"].values)})
  64. def select_clean_sources(
  65. datasets: Dict[str, xr.Dataset]
  66. ) -> Dict[str, Tuple[str, xr.Dataset]]:
  67. """Best clean (baseline) evaluation per family: ``{family: (stem, ds)}``."""
  68. chosen: Dict[str, Tuple[str, xr.Dataset]] = {}
  69. for stem in sorted(datasets, key=lambda s: source_rank(s, datasets[s])):
  70. for family in _families_in(datasets[stem]):
  71. chosen.setdefault(family, (stem, datasets[stem]))
  72. return chosen
  73. def select_noise_sources(
  74. datasets: Dict[str, xr.Dataset]
  75. ) -> Dict[str, Tuple[str, xr.Dataset]]:
  76. """Best noise-sweep evaluation per family: ``{family: (stem, ds)}``.
  77. Only datasets with more than one noise level qualify; families without a
  78. sweep are simply absent from the result.
  79. """
  80. chosen: Dict[str, Tuple[str, xr.Dataset]] = {}
  81. candidates = {s: d for s, d in datasets.items() if has_noise_sweep(d)}
  82. for stem in sorted(candidates, key=lambda s: noise_source_rank(s, candidates[s])):
  83. for family in _families_in(candidates[stem]):
  84. chosen.setdefault(family, (stem, candidates[stem]))
  85. return chosen