bayesian_noise.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. # bayesian_noise.py
  2. # ============================================================
  3. # Bayesian noise propagation on RAW X
  4. # Reference location and Δ widths are evaluated at exact clean-fit x50
  5. # Function-only module for notebook use
  6. # ============================================================
  7. import numpy as np
  8. import matplotlib.pyplot as plt
  9. from scipy.optimize import brentq
  10. from bayesian import load_xy, fit_bayes, P_with, make_trimmed_dataset
  11. # ---------------- Defaults ----------------
  12. XMAX = 5.0
  13. GRID_N = 1000
  14. SIGMA_MULT = 0.129
  15. SIGMA_ADD = 0.144
  16. N_REFIT = 150
  17. N_TTA = 300
  18. SEED = 1234
  19. CI_LEVEL = 0.95
  20. ALPHA = 1.0 - CI_LEVEL
  21. Q_LO, Q_MD, Q_HI = ALPHA / 2.0, 0.5, 1.0 - ALPHA / 2.0
  22. USE_PRIOR_P = True
  23. PRIOR_R = (1.05, 1.05)
  24. TAU = 25.0
  25. plt.rcParams["legend.frameon"] = False
  26. plt.rcParams["axes.titleweight"] = "bold"
  27. plt.rcParams["axes.labelweight"] = "bold"
  28. # ============================================================
  29. # 1) DATA
  30. # ============================================================
  31. def load_original(
  32. perc=95,
  33. suv_path="suv_percentilesSLOthenUWM.mat",
  34. flags_path="flags_combined.mat",
  35. ):
  36. return load_xy(perc=perc, suv_path=suv_path, flags_path=flags_path)
  37. def load_trimmed(
  38. perc=95,
  39. suv_path="suv_percentilesSLOthenUWM.mat",
  40. flags_path="flags_combined.mat",
  41. value_to_drop=2.48122597,
  42. tol=1e-3,
  43. ):
  44. X, y = load_xy(perc=perc, suv_path=suv_path, flags_path=flags_path)
  45. ds = make_trimmed_dataset(X, y, value_to_drop=value_to_drop, tol=tol)
  46. return ds["X_trim"], ds["y_trim"]
  47. # ============================================================
  48. # 2) FIT + X50
  49. # ============================================================
  50. def fit_once(X, y, rng, use_prior_p=USE_PRIOR_P, prior_r=PRIOR_R, tau=TAU):
  51. theta, res = fit_bayes(
  52. X,
  53. y,
  54. seed=int(rng.integers(0, 10_000_000)),
  55. use_prior_p=use_prior_p,
  56. prior_r=prior_r,
  57. tau=tau,
  58. )
  59. return theta, res
  60. def x_at_p(theta_hat, p_target=0.5, lo=1e-6, hi=6.0, hi_max=100.0):
  61. """
  62. Exact root solve for P(AE|x)=p_target.
  63. """
  64. f = lambda x: P_with(theta_hat, x) - p_target
  65. fa, fb = f(lo), f(hi)
  66. while np.isfinite(fa) and np.isfinite(fb) and fa * fb > 0 and hi < hi_max:
  67. hi *= 2.0
  68. fb = f(hi)
  69. if (not np.isfinite(fa)) or (not np.isfinite(fb)) or fa * fb > 0:
  70. return np.nan
  71. try:
  72. return float(brentq(f, lo, hi))
  73. except Exception:
  74. return np.nan
  75. # ============================================================
  76. # 3) NOISE
  77. # ============================================================
  78. def add_noise_mult(X, sigma, rng):
  79. X = np.asarray(X, float)
  80. return np.clip(X * np.exp(rng.normal(0, sigma, size=X.shape)), 1e-12, None)
  81. def add_noise_add(X, sigma, rng):
  82. X = np.asarray(X, float)
  83. return np.clip(X + rng.normal(0, sigma, size=X.shape), 1e-12, None)
  84. # ============================================================
  85. # 4) BAND HELPERS
  86. # ============================================================
  87. def monotone_nondec(y):
  88. y = np.asarray(y, float)
  89. return np.maximum.accumulate(np.clip(y, 0, 1))
  90. def band_quantiles(curves, q_lo=Q_LO, q_md=Q_MD, q_hi=Q_HI):
  91. C = np.vstack(curves)
  92. ql = monotone_nondec(np.quantile(C, q_lo, axis=0))
  93. qm = monotone_nondec(np.quantile(C, q_md, axis=0))
  94. qh = monotone_nondec(np.quantile(C, q_hi, axis=0))
  95. return ql, qm, qh
  96. def delta_width_at_x(lo, hi, xc, x0):
  97. if not np.isfinite(x0):
  98. return np.nan
  99. lo_x = float(np.interp(x0, xc, lo))
  100. hi_x = float(np.interp(x0, xc, hi))
  101. return hi_x - lo_x
  102. # ============================================================
  103. # 5) CORE ANALYSIS
  104. # ============================================================
  105. def build_bands_for_dataset(
  106. X,
  107. y,
  108. sigma_mult=SIGMA_MULT,
  109. sigma_add=SIGMA_ADD,
  110. x_max=XMAX,
  111. grid_n=GRID_N,
  112. n_refit=N_REFIT,
  113. n_tta=N_TTA,
  114. seed=SEED,
  115. use_prior_p=USE_PRIOR_P,
  116. prior_r=PRIOR_R,
  117. tau=TAU,
  118. ):
  119. rng = np.random.default_rng(seed)
  120. xc = np.linspace(1e-12, x_max, grid_n)
  121. theta_clean, res_clean = fit_once(
  122. X, y, rng,
  123. use_prior_p=use_prior_p,
  124. prior_r=prior_r,
  125. tau=tau,
  126. )
  127. clean_curve = P_with(theta_clean, xc)
  128. x50 = x_at_p(theta_clean, p_target=0.5, lo=1e-6, hi=max(6.0, x_max))
  129. # multiplicative: refit
  130. curves_refit_m = []
  131. for _ in range(n_refit):
  132. Xn = add_noise_mult(X, sigma_mult, rng)
  133. thetab, resb = fit_once(Xn, y, rng, use_prior_p=use_prior_p, prior_r=prior_r, tau=tau)
  134. if resb.success and np.isfinite(resb.fun):
  135. curves_refit_m.append(P_with(thetab, xc))
  136. if not curves_refit_m:
  137. raise RuntimeError("No successful multiplicative-refit curves.")
  138. lo_rm, md_rm, hi_rm = band_quantiles(curves_refit_m)
  139. # multiplicative: TTA
  140. curves_tta_m = []
  141. for _ in range(n_tta):
  142. xc_n = np.clip(xc * np.exp(rng.normal(0, sigma_mult, size=xc.size)), 1e-12, None)
  143. curves_tta_m.append(P_with(theta_clean, xc_n))
  144. lo_tm, md_tm, hi_tm = band_quantiles(curves_tta_m)
  145. # additive: refit
  146. curves_refit_a = []
  147. for _ in range(n_refit):
  148. Xn = add_noise_add(X, sigma_add, rng)
  149. thetab, resb = fit_once(Xn, y, rng, use_prior_p=use_prior_p, prior_r=prior_r, tau=tau)
  150. if resb.success and np.isfinite(resb.fun):
  151. curves_refit_a.append(P_with(thetab, xc))
  152. if not curves_refit_a:
  153. raise RuntimeError("No successful additive-refit curves.")
  154. lo_ra, md_ra, hi_ra = band_quantiles(curves_refit_a)
  155. # additive: TTA
  156. curves_tta_a = []
  157. for _ in range(n_tta):
  158. xc_n = np.clip(xc + rng.normal(0, sigma_add, size=xc.size), 1e-12, None)
  159. curves_tta_a.append(P_with(theta_clean, xc_n))
  160. lo_ta, md_ta, hi_ta = band_quantiles(curves_tta_a)
  161. return {
  162. "xc": xc,
  163. "theta_clean": theta_clean,
  164. "fit_success": bool(res_clean.success),
  165. "clean_curve": clean_curve,
  166. "x50": x50,
  167. "mult": {
  168. "sigma": sigma_mult,
  169. "refit": (lo_rm, md_rm, hi_rm),
  170. "tta": (lo_tm, md_tm, hi_tm),
  171. "D_refit": delta_width_at_x(lo_rm, hi_rm, xc, x50),
  172. "D_tta": delta_width_at_x(lo_tm, hi_tm, xc, x50),
  173. "n_refit_success": len(curves_refit_m),
  174. "n_tta": len(curves_tta_m),
  175. },
  176. "add": {
  177. "sigma": sigma_add,
  178. "refit": (lo_ra, md_ra, hi_ra),
  179. "tta": (lo_ta, md_ta, hi_ta),
  180. "D_refit": delta_width_at_x(lo_ra, hi_ra, xc, x50),
  181. "D_tta": delta_width_at_x(lo_ta, hi_ta, xc, x50),
  182. "n_refit_success": len(curves_refit_a),
  183. "n_tta": len(curves_tta_a),
  184. },
  185. }
  186. def run_noise_analysis(
  187. perc=95,
  188. suv_path="suv_percentilesSLOthenUWM.mat",
  189. flags_path="flags_combined.mat",
  190. value_to_drop=2.48122597,
  191. tol=1e-3,
  192. sigma_mult=SIGMA_MULT,
  193. sigma_add=SIGMA_ADD,
  194. x_max=XMAX,
  195. grid_n=GRID_N,
  196. n_refit=N_REFIT,
  197. n_tta=N_TTA,
  198. seed=SEED,
  199. use_prior_p=USE_PRIOR_P,
  200. prior_r=PRIOR_R,
  201. tau=TAU,
  202. ):
  203. X_full, y_full = load_original(
  204. perc=perc, suv_path=suv_path, flags_path=flags_path
  205. )
  206. X_trim, y_trim = load_trimmed(
  207. perc=perc, suv_path=suv_path, flags_path=flags_path,
  208. value_to_drop=value_to_drop, tol=tol
  209. )
  210. full_res = build_bands_for_dataset(
  211. X_full, y_full,
  212. sigma_mult=sigma_mult, sigma_add=sigma_add,
  213. x_max=x_max, grid_n=grid_n,
  214. n_refit=n_refit, n_tta=n_tta,
  215. seed=seed,
  216. use_prior_p=use_prior_p, prior_r=prior_r, tau=tau,
  217. )
  218. trim_res = build_bands_for_dataset(
  219. X_trim, y_trim,
  220. sigma_mult=sigma_mult, sigma_add=sigma_add,
  221. x_max=x_max, grid_n=grid_n,
  222. n_refit=n_refit, n_tta=n_tta,
  223. seed=seed + 100,
  224. use_prior_p=use_prior_p, prior_r=prior_r, tau=tau,
  225. )
  226. return {"full": full_res, "trim": trim_res}
  227. # ============================================================
  228. # 6) SUMMARY TABLE
  229. # ============================================================
  230. def make_noise_summary_table(noise_res):
  231. rows = []
  232. for dataset_name, block in [("FULL", noise_res["full"]), ("TRIM", noise_res["trim"])]:
  233. for noise_name, key in [("multiplicative", "mult"), ("additive", "add")]:
  234. rows.append({
  235. "Dataset": dataset_name,
  236. "NoiseType": noise_name,
  237. "Sigma": float(block[key]["sigma"]),
  238. "x50": float(block["x50"]),
  239. "Delta_refit_at_x50": float(block[key]["D_refit"]),
  240. "Delta_tta_at_x50": float(block[key]["D_tta"]),
  241. "N_refit_success": int(block[key]["n_refit_success"]),
  242. "N_tta": int(block[key]["n_tta"]),
  243. })
  244. import pandas as pd
  245. return pd.DataFrame(rows)
  246. # ============================================================
  247. # 7) PLOTTING
  248. # ============================================================
  249. def plot_4panels(
  250. full_res,
  251. trim_res,
  252. save_prefix="noise_cb_raw_4panels_sigma129_0144_x50",
  253. x_max=XMAX,
  254. sigma_mult=SIGMA_MULT,
  255. sigma_add=SIGMA_ADD,
  256. ):
  257. fig, axs = plt.subplots(2, 2, figsize=(12.5, 7.5), dpi=160, sharex=True, sharey=True)
  258. COL = {"mult": "#1f78b4", "add": "#ff7f00"}
  259. ALP = {"refit": 0.25, "tta": 0.12}
  260. def draw_panel(ax, res, mode, letter, dataset_label):
  261. xc = res["xc"]
  262. clean = res["clean_curve"]
  263. x50 = float(res["x50"])
  264. lo_r, _, hi_r = res[mode]["refit"]
  265. lo_t, _, hi_t = res[mode]["tta"]
  266. Dref = res[mode]["D_refit"]
  267. Dtta = res[mode]["D_tta"]
  268. ax.plot(xc, clean, color="k", lw=2.6, zorder=3)
  269. ax.fill_between(xc, lo_r, hi_r, color=COL[mode], alpha=ALP["refit"], zorder=1)
  270. ax.fill_between(xc, lo_t, hi_t, color=COL[mode], alpha=ALP["tta"], zorder=0)
  271. # exact x50 reference
  272. if np.isfinite(x50):
  273. ax.axvline(x50, color="#666", ls="--", lw=1.2, alpha=0.9, zorder=4)
  274. # vertical band-width markers at x50
  275. lo_r_x = float(np.interp(x50, xc, lo_r))
  276. hi_r_x = float(np.interp(x50, xc, hi_r))
  277. lo_t_x = float(np.interp(x50, xc, lo_t))
  278. hi_t_x = float(np.interp(x50, xc, hi_t))
  279. ax.plot([x50, x50], [lo_r_x, hi_r_x], color=COL[mode], lw=2.2, alpha=0.95, zorder=6)
  280. ax.scatter([x50, x50], [lo_r_x, hi_r_x], color=COL[mode], s=18, alpha=0.95, zorder=7)
  281. ax.plot([x50, x50], [lo_t_x, hi_t_x], color=COL[mode], lw=2.2, alpha=0.45, zorder=5)
  282. ax.scatter([x50, x50], [lo_t_x, hi_t_x], color=COL[mode], s=18, alpha=0.45, zorder=6)
  283. ax.set_title(letter, fontsize=14, fontweight="bold", pad=8)
  284. ax.text(
  285. 0.02, 0.96, dataset_label,
  286. transform=ax.transAxes, ha="left", va="top",
  287. fontsize=10, color="#111"
  288. )
  289. ax.text(
  290. 0.02, 0.88, f"x50={x50:.2f}",
  291. transform=ax.transAxes, ha="left", va="top",
  292. fontsize=10, color="#111"
  293. )
  294. ax.text(
  295. 0.03, 0.10, f"Δr@x50={Dref:.2f} Δt@x50={Dtta:.2f}",
  296. transform=ax.transAxes, ha="left", va="center", fontsize=9,
  297. bbox=dict(
  298. facecolor="white",
  299. edgecolor=COL[mode],
  300. boxstyle="round,pad=0.25,rounding_size=0.02",
  301. lw=0.9, alpha=0.95
  302. )
  303. )
  304. ax.set_xlim(0, x_max)
  305. ax.set_ylim(-0.05, 1.05)
  306. ax.grid(alpha=0.25)
  307. draw_panel(axs[0, 0], full_res, "mult", "A", "FULL (F)")
  308. draw_panel(axs[0, 1], full_res, "add", "B", "FULL (F)")
  309. draw_panel(axs[1, 0], trim_res, "mult", "C", "TRIM (T)")
  310. draw_panel(axs[1, 1], trim_res, "add", "D", "TRIM (T)")
  311. axs[1, 0].set_xlabel("X")
  312. axs[1, 1].set_xlabel("X")
  313. axs[0, 0].set_ylabel("P(AE | x)")
  314. axs[1, 0].set_ylabel("P(AE | x)")
  315. handles = [
  316. plt.Line2D([0], [0], color="k", lw=2.6, label="clean Bayesian fit"),
  317. plt.Rectangle((0, 0), 1, 1, facecolor=COL["mult"], alpha=ALP["refit"],
  318. label=f"refit band, mult σ={sigma_mult:.3f}"),
  319. plt.Rectangle((0, 0), 1, 1, facecolor=COL["mult"], alpha=ALP["tta"],
  320. label=f"TTA band, mult σ={sigma_mult:.3f}"),
  321. plt.Rectangle((0, 0), 1, 1, facecolor=COL["add"], alpha=ALP["refit"],
  322. label=f"refit band, add σ={sigma_add:.3f}"),
  323. plt.Rectangle((0, 0), 1, 1, facecolor=COL["add"], alpha=ALP["tta"],
  324. label=f"TTA band, add σ={sigma_add:.3f}"),
  325. plt.Line2D([0], [0], color="#666", lw=1.2, ls="--", label="x50"),
  326. ]
  327. fig.legend(handles, [h.get_label() for h in handles],
  328. loc="lower center", ncol=3, fontsize=10)
  329. plt.tight_layout(rect=[0, 0.08, 1, 1])
  330. if save_prefix is not None:
  331. fig.savefig(f"{save_prefix}.png", dpi=300, bbox_inches="tight")
  332. fig.savefig(f"{save_prefix}.pdf", bbox_inches="tight")
  333. return fig, axs