| from __future__ import annotations |
|
|
| import math |
| import sys |
| from dataclasses import dataclass |
| from pathlib import Path |
|
|
| import numpy as np |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| DEFAULT_DUMP_PATH = ROOT / "eval" / "probe_dump.npz" |
|
|
|
|
| @dataclass(frozen=True) |
| class BocpdResult: |
| step: int |
| value: float |
| cp_prob: float |
| map_run_length: int |
| change_point: bool |
|
|
|
|
| def logsumexp(values: np.ndarray) -> float: |
| max_value = float(np.max(values)) |
| if not math.isfinite(max_value): |
| return max_value |
| return max_value + math.log(float(np.sum(np.exp(values - max_value)))) |
|
|
|
|
| def student_t_logpdf(x: float, mu: np.ndarray, kappa: np.ndarray, alpha: np.ndarray, beta: np.ndarray) -> np.ndarray: |
| nu = 2.0 * alpha |
| scale = np.sqrt(beta * (kappa + 1.0) / (alpha * kappa)) |
| z = (x - mu) / scale |
| return ( |
| np.vectorize(math.lgamma)((nu + 1.0) / 2.0) |
| - np.vectorize(math.lgamma)(nu / 2.0) |
| - 0.5 * np.log(nu * math.pi) |
| - np.log(scale) |
| - ((nu + 1.0) / 2.0) * np.log1p((z * z) / nu) |
| ) |
|
|
|
|
| def update_nig( |
| x: float, |
| mu: np.ndarray, |
| kappa: np.ndarray, |
| alpha: np.ndarray, |
| beta: np.ndarray, |
| ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: |
| next_kappa = kappa + 1.0 |
| next_mu = (kappa * mu + x) / next_kappa |
| next_alpha = alpha + 0.5 |
| next_beta = beta + 0.5 * kappa * (x - mu) ** 2 / next_kappa |
| return next_mu, next_kappa, next_alpha, next_beta |
|
|
|
|
| def run_bocpd( |
| values: np.ndarray, |
| hazard: float = 1.0 / 50.0, |
| prior_mu: float = 0.0, |
| prior_kappa: float = 1.0e-3, |
| prior_alpha: float = 1.0, |
| prior_beta: float = 1.0, |
| ) -> list[BocpdResult]: |
| log_hazard = math.log(hazard) |
| log_growth_factor = math.log1p(-hazard) |
|
|
| log_run_probs = np.asarray([0.0], dtype=np.float64) |
| mu = np.asarray([prior_mu], dtype=np.float64) |
| kappa = np.asarray([prior_kappa], dtype=np.float64) |
| alpha = np.asarray([prior_alpha], dtype=np.float64) |
| beta = np.asarray([prior_beta], dtype=np.float64) |
|
|
| results: list[BocpdResult] = [] |
| previous_map_run_length: int | None = None |
|
|
| for step, x_value in enumerate(values, start=1): |
| x = float(x_value) |
| predictive = student_t_logpdf(x, mu, kappa, alpha, beta) |
|
|
| growth_probs = log_run_probs + predictive + log_growth_factor |
| cp_prob = logsumexp(log_run_probs + predictive + log_hazard) |
| new_log_run_probs = np.concatenate(([cp_prob], growth_probs)) |
| normalizer = logsumexp(new_log_run_probs) |
| new_log_run_probs -= normalizer |
|
|
| grown_mu, grown_kappa, grown_alpha, grown_beta = update_nig(x, mu, kappa, alpha, beta) |
| mu = np.concatenate(([prior_mu], grown_mu)) |
| kappa = np.concatenate(([prior_kappa], grown_kappa)) |
| alpha = np.concatenate(([prior_alpha], grown_alpha)) |
| beta = np.concatenate(([prior_beta], grown_beta)) |
| log_run_probs = new_log_run_probs |
|
|
| map_run_length = int(np.argmax(log_run_probs)) |
| cp_probability = float(np.exp(log_run_probs[0])) |
| change_point = bool( |
| map_run_length == 0 |
| or (previous_map_run_length is not None and map_run_length < previous_map_run_length) |
| ) |
| results.append( |
| BocpdResult( |
| step=step, |
| value=x, |
| cp_prob=cp_probability, |
| map_run_length=map_run_length, |
| change_point=change_point, |
| ) |
| ) |
| previous_map_run_length = map_run_length |
|
|
| return results |
|
|
|
|
| def main() -> None: |
| dump_path = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_DUMP_PATH |
| if not dump_path.is_absolute(): |
| dump_path = ROOT / dump_path |
|
|
| if not dump_path.exists(): |
| print(f"No probe dump found at {dump_path}") |
| return |
|
|
| dump = np.load(dump_path, allow_pickle=True) |
| failure = str(dump.get("failure", "")) |
| values = np.asarray(dump["nll_series"], dtype=np.float64) |
| if values.size == 0: |
| print("No NLL samples found; skipping BOCPD.") |
| if failure: |
| print(f"Probe failure: {failure}") |
| return |
|
|
| print("step | nll | cp_prob | map_run_length | change_point") |
| print("-----|-----|---------|----------------|-------------") |
| for result in run_bocpd(values): |
| flag = "YES" if result.change_point else "no" |
| print( |
| f"{result.step:>4} | {result.value:.4f} | {result.cp_prob:.4f} | " |
| f"{result.map_run_length:>14} | {flag}" |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|