| """Run a synthetic SimplexUQ benchmark experiment from config.""" |
| import argparse |
| import json |
| import logging |
| from pathlib import Path |
| import sys |
| import time |
|
|
| import numpy as np |
| import yaml |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) |
|
|
| from src.dgp.discrete_groups import DiscreteGroupsDGP |
| from src.dgp.heavy_tail import HeavyTailDGP |
| from src.dgp.high_k import HighKDGP |
| from src.dgp.model_bias import ModelBiasDGP |
| from src.dgp.pure_scale import PureScaleDGP |
| from src.methods import ( |
| full_conformal, |
| global_split_conformal, |
| jackknife_plus_conformal, |
| oneshot_conformal, |
| oracle_conformal, |
| partition_conformal, |
| trainres_conformal, |
| twostage_conformal, |
| weighted_conformal, |
| ) |
| from src.methods._knn_sigma import knn_sigma_hat, knn_sigma_leave_one_out |
| from src.metrics import ( |
| coverage_variance, |
| marginal_coverage, |
| max_disparity, |
| mean_radius, |
| mean_volume_ratio, |
| size_stratified_coverage_violation, |
| stratified_coverage, |
| volume_ratio_by_strata, |
| worst_stratum_coverage, |
| ) |
| from src.utils.seed import get_rng |
| from src.utils.strata import ( |
| precompute_fixed_strata, |
| stratify_by_argmax_group, |
| stratify_by_boundary, |
| stratify_by_entropy, |
| stratify_by_kmeans, |
| ) |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") |
| log = logging.getLogger(__name__) |
|
|
| DGP_MAP = { |
| "pure_scale": PureScaleDGP, |
| "model_bias": ModelBiasDGP, |
| "discrete_groups": DiscreteGroupsDGP, |
| "heavy_tail": HeavyTailDGP, |
| "high_k": HighKDGP, |
| } |
|
|
| STRATA_MAP = { |
| "boundary": stratify_by_boundary, |
| "entropy": stratify_by_entropy, |
| "kmeans": stratify_by_kmeans, |
| } |
|
|
| DEFAULT_METHODS = [ |
| "global", |
| "fullcp", |
| "jackknife_plus", |
| "partition", |
| "twostage", |
| "oneshot", |
| "trainres", |
| "weighted", |
| "oracle", |
| ] |
|
|
|
|
| def get_strata(U: np.ndarray, cfg: dict) -> np.ndarray: |
| method = cfg["evaluation"]["strata_method"] |
| if method == "argmax_group": |
| split_index = cfg["evaluation"].get("split_index", cfg["dgp"].get("easy_classes", 5)) |
| return stratify_by_argmax_group(U, split_index=split_index) |
| return STRATA_MAP[method](U, cfg["evaluation"]["n_strata"]) |
|
|
|
|
| def get_fixed_strata_labels(U: np.ndarray, cfg: dict) -> np.ndarray: |
| """Compute one prediction-space stratification map before cal/test splitting.""" |
| method = cfg["evaluation"]["strata_method"] |
| if method == "argmax_group": |
| split_index = cfg["evaluation"].get("split_index", cfg["dgp"].get("easy_classes", 5)) |
| return stratify_by_argmax_group(U, split_index=split_index) |
| return precompute_fixed_strata( |
| U, |
| method, |
| cfg["evaluation"]["n_strata"], |
| seed=cfg.get("seed", 2026), |
| ) |
|
|
|
|
| def get_alpha(cfg: dict) -> float: |
| if "conformal" in cfg: |
| return cfg["conformal"]["alpha"] |
| return cfg["evaluation"]["alpha"] |
|
|
|
|
| def get_methods(cfg: dict) -> list[str]: |
| if "conformal" in cfg and "methods" in cfg["conformal"]: |
| return list(cfg["conformal"]["methods"]) |
| return list(cfg.get("methods", DEFAULT_METHODS)) |
|
|
|
|
| def get_method_params(cfg: dict, method_name: str) -> dict: |
| return dict(cfg.get("method_params", {}).get(method_name, {})) |
|
|
|
|
| def compute_weight_vectors( |
| cfg: dict, |
| R_cal: np.ndarray, |
| U_cal: np.ndarray, |
| U_test: np.ndarray, |
| sigma_cal_true: np.ndarray | None, |
| sigma_test_true: np.ndarray | None, |
| ) -> tuple[np.ndarray, np.ndarray]: |
| """Build weighted-CP importance weights. |
| |
| Defaults to inverse local scale so that hard regions receive more mass. |
| """ |
| weight_cfg = cfg.get("weighting", {}) |
| mode = weight_cfg.get("mode", "inverse_sigma") |
| source = weight_cfg.get("source", "knn") |
| eps = float(weight_cfg.get("eps", 1e-8)) |
|
|
| if mode != "inverse_sigma": |
| raise ValueError(f"Unsupported weighting mode: {mode}") |
|
|
| if source == "oracle" and sigma_cal_true is not None and sigma_test_true is not None: |
| sigma_cal = sigma_cal_true |
| sigma_test = sigma_test_true |
| elif source == "knn_loo": |
| sigma_cal = knn_sigma_leave_one_out(U_cal, R_cal, k=weight_cfg.get("k", 20)) |
| sigma_test = knn_sigma_hat(U_cal, R_cal, U_test, k=weight_cfg.get("k", 20)) |
| else: |
| sigma_cal = knn_sigma_hat(U_cal, R_cal, U_cal, k=weight_cfg.get("k", 20)) |
| sigma_test = knn_sigma_hat(U_cal, R_cal, U_test, k=weight_cfg.get("k", 20)) |
|
|
| weights_cal = 1.0 / np.maximum(sigma_cal, eps) |
| weights_test = 1.0 / np.maximum(sigma_test, eps) |
|
|
| |
| weights_cal = weights_cal / np.mean(weights_cal) |
| weights_test = weights_test / np.mean(weights_test) |
| return weights_cal, weights_test |
|
|
|
|
| def evaluate_method( |
| res, |
| U_test: np.ndarray, |
| strata_test: np.ndarray, |
| alpha: float, |
| runtime_sec: float, |
| cfg: dict, |
| ) -> dict: |
| metrics = { |
| "marginal_coverage": float(marginal_coverage(res.covered)), |
| "max_disparity": float(max_disparity(res.covered, strata_test, alpha)), |
| "worst_stratum_coverage": float(worst_stratum_coverage(res.covered, strata_test)), |
| "mean_radius": float(mean_radius(res.radius)), |
| "sscv": float(size_stratified_coverage_violation(res.covered, res.radius, alpha)), |
| "coverage_variance": float(coverage_variance(res.covered, strata_test)), |
| "runtime_sec": float(runtime_sec), |
| "stratified_coverage": { |
| str(k): float(v) for k, v in stratified_coverage(res.covered, strata_test).items() |
| }, |
| } |
| volume_cfg = cfg["evaluation"].get("volume", {}) |
| if volume_cfg.get("compute", False): |
| score = volume_cfg.get("score", "aitchison") |
| n_mc = int(volume_cfg.get("n_mc", 20000)) |
| max_points = volume_cfg.get("max_points") |
| seed = int(volume_cfg.get("seed", 0)) |
| metrics["mean_volume_ratio"] = float( |
| mean_volume_ratio( |
| U_test, |
| res.radius, |
| score=score, |
| n_mc=n_mc, |
| max_points=max_points, |
| rng=np.random.default_rng(seed), |
| ) |
| ) |
| metrics["volume_ratio_by_strata"] = { |
| str(k): float(v) |
| for k, v in volume_ratio_by_strata( |
| U_test, |
| res.radius, |
| strata_test, |
| score=score, |
| n_mc=n_mc, |
| max_points=max_points, |
| rng=np.random.default_rng(seed), |
| ).items() |
| } |
| return metrics |
|
|
|
|
| def run_one_rep(dgp, cfg: dict, rng: np.random.Generator) -> dict: |
| """Generate one synthetic draw and evaluate the requested methods.""" |
| dcfg = cfg["data"] |
| ecfg = cfg["evaluation"] |
| alpha = get_alpha(cfg) |
|
|
| n_total = dcfg["n_cal"] + dcfg["n_test"] |
| sample = dgp.sample(n_total, rng) |
|
|
| idx = rng.permutation(n_total) |
| idx_cal = idx[:dcfg["n_cal"]] |
| idx_test = idx[dcfg["n_cal"]:] |
|
|
| R_cal, R_test = sample.R[idx_cal], sample.R[idx_test] |
| U_cal, U_test = sample.U[idx_cal], sample.U[idx_test] |
| sigma_cal = sample.sigma_true[idx_cal] if sample.sigma_true is not None else None |
| sigma_test = sample.sigma_true[idx_test] if sample.sigma_true is not None else None |
|
|
| fixed_strata = get_fixed_strata_labels(sample.U, cfg) |
| strata_cal = fixed_strata[idx_cal] |
| strata_test = fixed_strata[idx_test] |
| weights_cal, weights_test = compute_weight_vectors( |
| cfg, R_cal, U_cal, U_test, sigma_cal, sigma_test |
| ) |
|
|
| results = {} |
| methods = get_methods(cfg) |
|
|
| for method_name in methods: |
| method_params = get_method_params(cfg, method_name) |
| start = time.perf_counter() |
|
|
| if method_name == "global": |
| res = global_split_conformal(R_cal, R_test, alpha) |
| elif method_name == "partition": |
| res = partition_conformal(R_cal, R_test, alpha, strata_cal, strata_test) |
| elif method_name == "twostage": |
| res = twostage_conformal( |
| R_cal, |
| R_test, |
| alpha, |
| U_cal, |
| U_test, |
| n_scale_est=dcfg.get("n_scale_est"), |
| k=method_params.get("k", 20), |
| ) |
| elif method_name == "fullcp": |
| res = full_conformal( |
| R_cal, |
| R_test, |
| alpha, |
| U_cal, |
| U_test, |
| k=method_params.get("k", 20), |
| ) |
| elif method_name == "jackknife_plus": |
| res = jackknife_plus_conformal( |
| R_cal, |
| R_test, |
| alpha, |
| U_cal=U_cal, |
| U_test=U_test, |
| loo_scores=None, |
| k=method_params.get("k", 20), |
| ) |
| elif method_name == "oracle": |
| if sigma_cal is None or sigma_test is None: |
| log.warning("Skipping oracle: true sigma unavailable") |
| continue |
| res = oracle_conformal(R_cal, R_test, alpha, sigma_cal, sigma_test) |
| elif method_name == "oneshot": |
| res = oneshot_conformal( |
| R_cal, |
| R_test, |
| alpha, |
| U_cal, |
| U_test, |
| k=method_params.get("k", 20), |
| ) |
| elif method_name == "trainres": |
| n_train = dcfg.get("n_train", dcfg["n_cal"]) |
| train_sample = dgp.sample(n_train, rng) |
| res = trainres_conformal( |
| R_cal, |
| R_test, |
| alpha, |
| U_cal, |
| U_test, |
| train_sample.R, |
| train_sample.U, |
| k=method_params.get("k", 20), |
| ) |
| elif method_name == "weighted": |
| res = weighted_conformal(R_cal, R_test, alpha, weights_cal, weights_test) |
| else: |
| log.warning("Unknown method: %s", method_name) |
| continue |
|
|
| runtime_sec = time.perf_counter() - start |
| results[method_name] = evaluate_method( |
| res, |
| U_test, |
| strata_test, |
| alpha, |
| runtime_sec=runtime_sec, |
| cfg=cfg, |
| ) |
|
|
| return results |
|
|
|
|
| def aggregate_repetitions(all_results: dict[str, list[dict]]) -> dict: |
| """Aggregate scalar and per-stratum metrics over repetitions.""" |
| scalar_keys = [ |
| "marginal_coverage", |
| "max_disparity", |
| "worst_stratum_coverage", |
| "mean_radius", |
| "sscv", |
| "coverage_variance", |
| "runtime_sec", |
| "mean_volume_ratio", |
| ] |
|
|
| summary = {} |
| for method_name, reps in all_results.items(): |
| if not reps: |
| continue |
|
|
| summary[method_name] = {} |
| for key in scalar_keys: |
| if key in reps[0]: |
| values = [rep[key] for rep in reps] |
| summary[method_name][key] = { |
| "mean": float(np.mean(values)), |
| "std": float(np.std(values)), |
| } |
|
|
| all_strata_keys = set() |
| for rep in reps: |
| all_strata_keys.update(rep["stratified_coverage"].keys()) |
|
|
| summary[method_name]["stratified_coverage"] = {} |
| for stratum in sorted(all_strata_keys, key=int): |
| values = [ |
| rep["stratified_coverage"][stratum] |
| for rep in reps |
| if stratum in rep["stratified_coverage"] |
| ] |
| summary[method_name]["stratified_coverage"][stratum] = { |
| "mean": float(np.mean(values)), |
| "std": float(np.std(values)), |
| "n_reps": len(values), |
| } |
|
|
| if "volume_ratio_by_strata" in reps[0]: |
| all_vol_keys = set() |
| for rep in reps: |
| all_vol_keys.update(rep["volume_ratio_by_strata"].keys()) |
| summary[method_name]["volume_ratio_by_strata"] = {} |
| for stratum in sorted(all_vol_keys, key=int): |
| values = [ |
| rep["volume_ratio_by_strata"][stratum] |
| for rep in reps |
| if stratum in rep["volume_ratio_by_strata"] |
| ] |
| summary[method_name]["volume_ratio_by_strata"][stratum] = { |
| "mean": float(np.mean(values)), |
| "std": float(np.std(values)), |
| "n_reps": len(values), |
| } |
|
|
| return summary |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--config", required=True) |
| args = parser.parse_args() |
|
|
| with open(args.config) as f: |
| cfg = yaml.safe_load(f) |
|
|
| log.info("Experiment: %s", cfg["experiment"]) |
|
|
| dgp_cfg = cfg["dgp"] |
| dgp_name = dgp_cfg["name"] |
| dgp_cls = DGP_MAP[dgp_name] |
| dgp_kwargs = {k: v for k, v in dgp_cfg.items() if k != "name"} |
| dgp = dgp_cls(**dgp_kwargs) |
|
|
| n_rep = cfg["data"]["n_rep"] |
| methods = get_methods(cfg) |
| rng = get_rng(cfg["seed"]) |
| all_results = {method: [] for method in methods} |
|
|
| for rep_idx in range(n_rep): |
| rep_results = run_one_rep(dgp, cfg, rng) |
| for method_name, metrics in rep_results.items(): |
| all_results[method_name].append(metrics) |
|
|
| if (rep_idx + 1) % 25 == 0 or rep_idx == n_rep - 1: |
| log.info(" Completed %d/%d reps", rep_idx + 1, n_rep) |
|
|
| summary = aggregate_repetitions(all_results) |
|
|
| log.info("") |
| log.info("=" * 72) |
| log.info("RESULTS (mean +- std over %d reps)", n_rep) |
| log.info("=" * 72) |
| for method_name in methods: |
| if method_name not in summary: |
| continue |
| cov = summary[method_name]["marginal_coverage"]["mean"] |
| disp = summary[method_name]["max_disparity"]["mean"] |
| worst = summary[method_name]["worst_stratum_coverage"]["mean"] |
| radius = summary[method_name]["mean_radius"]["mean"] |
| sscv = summary[method_name]["sscv"]["mean"] |
| log.info( |
| " %-14s cov=%.3f disp=%.3f worst=%.3f radius=%.3f sscv=%.3f", |
| method_name, |
| cov, |
| disp, |
| worst, |
| radius, |
| sscv, |
| ) |
|
|
| out_dir = Path("results/tables") |
| out_dir.mkdir(parents=True, exist_ok=True) |
| out_file = out_dir / f"{cfg['experiment']}.json" |
| with open(out_file, "w") as f: |
| json.dump( |
| { |
| "config": cfg, |
| "methods": methods, |
| "summary": summary, |
| "raw": all_results, |
| }, |
| f, |
| indent=2, |
| ) |
| log.info("Saved to %s", out_file) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|