File size: 7,446 Bytes
fce6c09 3d60772 fce6c09 3d60772 fce6c09 3d60772 fce6c09 3d60772 43604b6 fce6c09 3d60772 fce6c09 baf1286 3d60772 baf1286 3d60772 baf1286 43604b6 3d60772 43604b6 0817e0a 4bcf26d 0817e0a 3d60772 0817e0a 3d60772 0817e0a fce6c09 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | from __future__ import annotations
import json
from pathlib import Path
# Taxonomy
# --------
# Every method carries four independent tags, each scoped to its own axis:
#
# ``parallelism`` β how the search explores parameter space:
# "serial" β a single point estimate advanced step by step (ADAM, LM).
# "parallel-independent" β a population of candidates updated with no coupling
# between members (ABC's accepted samples, HM's per-wave
# resampling from the non-implausible region).
# "parallel-interacting" β an ensemble whose members are coupled through a shared
# update each iteration (any Kalman-based method).
#
# ``update_type`` β the mechanism driving each update step:
# "gradient" β follows the loss gradient or a Gauss-Newton approximation of it.
# "kalman" β a (possibly linearized/unscented) Kalman-style ensemble update.
# "general" β anything else (e.g. ABC's rejection sampling, HM's implausibility cuts).
#
# ``method_goal`` β what the method is built to report:
# "optimization" β a single best-fit parameter estimate.
# "uq" β the full posterior / parameter uncertainty. UQ methods can still
# be scored on the Optimization leaderboard (usually less
# competitive there, since they're not optimizing for speed-to-target).
#
# ``emulator_use`` β when/whether a surrogate model of the forward model is used:
# "none" β samples/evaluates the true forward model throughout.
# "within-optimize" β refits a surrogate at each iteration of the search (HM waves).
# "after-optimize" β fits a surrogate once, after calibration finishes (CES).
#
# Note: Kalman methods are Bayesian in spirit too (approximate Gaussian posterior
# updates) β ``update_type`` is about mechanism, not a "Bayesian vs not" philosophy.
KNOWN_METHODS = {
"teki": {
"abbreviation": "TEKI",
"Method": "Tikhonov Regularized Ensemble Kalman Inversion",
"parallelism": "parallel-interacting",
"update_type": "kalman",
"method_goal": "optimization",
"emulator_use": "none",
"aliases": ["teki"],
},
"etki": {
"abbreviation": "ETKI",
"Method": "Ensemble Transform Kalman Inversion",
"parallelism": "parallel-interacting",
"update_type": "kalman",
"method_goal": "optimization",
"emulator_use": "none",
"aliases": ["etki"],
},
"iekf": {
"abbreviation": "IEKF",
"Method": "Iterative Ensemble Kalman Filter",
"parallelism": "parallel-interacting",
"update_type": "kalman",
"method_goal": "uq",
"emulator_use": "none",
"aliases": ["iekf", "gnsl", "gnki"],
},
"uki": {
"abbreviation": "UKI",
"Method": "Unscented Kalman Inversion",
"parallelism": "parallel-interacting",
"update_type": "kalman",
"method_goal": "optimization",
"emulator_use": "none",
"aliases": ["uki"],
},
"abc": {
"abbreviation": "ABC",
"Method": "Approximate Bayesian Calibration",
"parallelism": "parallel-independent",
"update_type": "general",
"method_goal": "uq",
"emulator_use": "none",
"aliases": ["abc"],
},
"hm": {
"abbreviation": "HM",
"Method": "History Matching",
"parallelism": "parallel-independent",
"update_type": "general",
"method_goal": "uq",
"emulator_use": "within-optimize",
"aliases": ["hm"],
},
"ces-eki-dmc": {
"abbreviation": "CES-EKI-DMC",
"Method": "Calibrate Emulate Sample (EKI-DataMisfitController)",
"parallelism": "parallel-interacting",
"update_type": "kalman",
"method_goal": "uq",
"emulator_use": "after-optimize",
"aliases": ["ces-eki-dmc"]
},
"ces-eki-const": {
"abbreviation": "CES-EKI-CONST",
"Method": "Calibrate Emulate Sample (EKI-Constant Scheduler)",
"parallelism": "parallel-interacting",
"update_type": "kalman",
"method_goal": "uq",
"emulator_use": "after-optimize",
"aliases": ["ces-eki-const"]
},
"ces-iekf-const": {
"abbreviation": "CES-IEKF-CONST",
"Method": "Calibrate Emulate Sample (IEKF-Constant Scheduler)",
"parallelism": "parallel-interacting",
"update_type": "kalman",
"method_goal": "uq",
"emulator_use": "after-optimize",
"aliases": ["ces-iekf-const"]
},
"adam": {
"abbreviation": "ADAM",
"Method": "Adaptive Moment Estimation",
"parallelism": "serial",
"update_type": "gradient",
"method_goal": "optimization",
"emulator_use": "none",
"aliases": ["adam"],
},
"lm": {
"abbreviation": "LM",
"Method": "Levenberg-Marquardt",
"parallelism": "serial",
"update_type": "gradient",
"method_goal": "optimization",
"emulator_use": "none",
"aliases": ["lm", "levenberg_marquardt", "levenberg-marquardt", "gradient_descent"],
},
}
# Vega tableau10 palette β one slot per method in KNOWN_METHODS declaration order.
# New methods appended to KNOWN_METHODS get the next slot; existing colors never shift.
_METHOD_PALETTE = [
"#4c78a8", "#f58518", "#e45756", "#72b7b2", "#54a24b",
"#eeca3b", "#b279a2", "#ff9da6", "#9d755d", "#bab0ac",
]
# Stable abbreviation β hex color mapping. Import this wherever Altair charts are built
# so every plot in the app assigns the same color to each method.
METHOD_COLORS: dict[str, str] = {
meta["abbreviation"]: _METHOD_PALETTE[i % len(_METHOD_PALETTE)]
for i, meta in enumerate(KNOWN_METHODS.values())
}
def build_alias_lookup() -> dict[str, str]:
lookup: dict[str, str] = {}
for canonical_name, meta in KNOWN_METHODS.items():
lookup[canonical_name] = canonical_name
lookup[canonical_name.upper()] = canonical_name
for alias in meta.get("aliases", []):
lookup[alias.lower()] = canonical_name
lookup[alias.upper()] = canonical_name
return lookup
ALIAS_TO_CANONICAL = build_alias_lookup()
def normalize_method_name(name: object) -> str:
text = str(name).strip()
if text.startswith("b'") and text.endswith("'"):
text = text[2:-1]
elif text.startswith('b"') and text.endswith('"'):
text = text[2:-1]
text = text.strip("\"'").strip()
return text.lower()
def canonicalize_method_name(name: object) -> str:
normalized = normalize_method_name(name)
return ALIAS_TO_CANONICAL.get(normalized, normalized)
def get_method_meta(canonical_name: str) -> dict[str, str]:
return KNOWN_METHODS.get(canonical_name, {})
def dump_method_registry_snapshot(project_root: Path, observed_methods: set[str]) -> None:
snapshot = {
"known_methods": KNOWN_METHODS,
"observed_methods": sorted(observed_methods),
"unmapped_observed_methods": sorted([method for method in observed_methods if method not in KNOWN_METHODS]),
}
cache_dir = project_root / ".cache"
cache_dir.mkdir(parents=True, exist_ok=True)
target_file = cache_dir / "known_methods_snapshot.json"
target_file.write_text(json.dumps(snapshot, indent=2), encoding="utf-8")
|