| |
| """Aggregate GRL SNR-transfer experiments across multiple random seeds.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import csv |
| import json |
| import math |
| from collections import defaultdict |
| from pathlib import Path |
| from statistics import mean, stdev |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[1] |
|
|
|
|
| PHASE_METRICS = [ |
| "P_precision", |
| "P_recall", |
| "P_f1", |
| "S_precision", |
| "S_recall", |
| "S_f1", |
| "mean_f1", |
| ] |
|
|
| DISP_METRICS = [ |
| "val_mae", |
| "val_rmse", |
| "val_certainty_f1", |
| ] |
|
|
|
|
| def read_summary(path: Path) -> dict: |
| if not path.exists(): |
| raise FileNotFoundError(path) |
| with path.open(encoding="utf-8") as f: |
| return json.load(f) |
|
|
|
|
| def stats(values: list[float]) -> dict[str, float]: |
| if not values: |
| return {"mean": math.nan, "std": math.nan, "n": 0} |
| return { |
| "mean": float(mean(values)), |
| "std": float(stdev(values)) if len(values) > 1 else 0.0, |
| "n": len(values), |
| } |
|
|
|
|
| def aggregate_task( |
| task: str, |
| seeds: list[int], |
| metrics: list[str], |
| out_dir: Path, |
| phase_balanced: bool = False, |
| phase_prefix: str | None = None, |
| ) -> tuple[list[dict], dict]: |
| by_condition: dict[str, dict[str, list[float]]] = defaultdict(lambda: defaultdict(list)) |
| condition_labels: dict[str, str] = {} |
| rows_long: list[dict] = [] |
| metadata: dict = {"seeds": seeds, "task": task, "summaries": []} |
|
|
| if task == "phase": |
| prefix = phase_prefix or ("snr_transfer_phase_balanced_seed" if phase_balanced else "snr_transfer_seed") |
| else: |
| prefix = "disp_snr_transfer_seed" |
| for seed in seeds: |
| path = ROOT / "outputs" / f"{prefix}{seed}" / "summary.json" |
| summary = read_summary(path) |
| metadata["summaries"].append(str(path.relative_to(ROOT))) |
| for row in summary["rows"]: |
| slug = row["slug"] |
| condition_labels.setdefault(slug, row["label"]) |
| for metric in metrics: |
| value = float(row[metric]) |
| by_condition[slug][metric].append(value) |
| rows_long.append( |
| { |
| "task": task, |
| "seed": seed, |
| "condition_slug": slug, |
| "condition_label": row["label"], |
| "metric": metric, |
| "value": value, |
| } |
| ) |
|
|
| rows_summary: list[dict] = [] |
| for condition, metric_values in by_condition.items(): |
| for metric, values in metric_values.items(): |
| s = stats(values) |
| rows_summary.append( |
| { |
| "task": task, |
| "condition_slug": condition, |
| "condition_label": condition_labels.get(condition, condition), |
| "metric": metric, |
| "mean": s["mean"], |
| "std": s["std"], |
| "n": s["n"], |
| "values": values, |
| } |
| ) |
|
|
| out_dir.mkdir(parents=True, exist_ok=True) |
| long_path = out_dir / f"{task}_multiseed_long.csv" |
| with long_path.open("w", newline="", encoding="utf-8") as f: |
| writer = csv.DictWriter(f, fieldnames=["task", "seed", "condition_slug", "condition_label", "metric", "value"]) |
| writer.writeheader() |
| writer.writerows(rows_long) |
|
|
| summary_path = out_dir / f"{task}_multiseed_summary.csv" |
| with summary_path.open("w", newline="", encoding="utf-8") as f: |
| writer = csv.DictWriter( |
| f, |
| fieldnames=["task", "condition_slug", "condition_label", "metric", "mean", "std", "n", "values"], |
| ) |
| writer.writeheader() |
| for row in rows_summary: |
| row = dict(row) |
| row["values"] = json.dumps(row["values"]) |
| writer.writerow(row) |
|
|
| return rows_summary, metadata |
|
|
|
|
| def lookup(rows: list[dict], task: str, condition: str, metric: str) -> dict: |
| for row in rows: |
| if row["task"] == task and row["condition_slug"] == condition and row["metric"] == metric: |
| return row |
| raise KeyError((task, condition, metric)) |
|
|
|
|
| def has_task(all_rows: list[dict], task: str) -> bool: |
| return any(row["task"] == task for row in all_rows) |
|
|
|
|
| def has_condition(all_rows: list[dict], task: str, condition: str) -> bool: |
| return any(row["task"] == task and row["condition_slug"] == condition for row in all_rows) |
|
|
|
|
| def condition_label(all_rows: list[dict], task: str, condition: str, fallback: str) -> str: |
| for row in all_rows: |
| if row["task"] == task and row["condition_slug"] == condition: |
| return row.get("condition_label") or fallback |
| return fallback |
|
|
|
|
| def write_markdown(all_rows: list[dict], out_dir: Path, phase_balanced: bool = False) -> None: |
| lines = [ |
| "# Multi-Seed SNR-Filtering Summary", |
| "", |
| "Values are mean +/- sample standard deviation across three seeds.", |
| "", |
| ] |
|
|
| if has_task(all_rows, "phase"): |
| lines.extend([ |
| "## Phase Picking", |
| "", |
| "| Training subset | Mean F1 | P F1 | S F1 |", |
| "|---|---:|---:|---:|", |
| ]) |
| if phase_balanced: |
| if has_condition(all_rows, "phase", "finetune_full"): |
| phase_labels = [ |
| ("finetune_full", "Fine-tune full matched"), |
| ("finetune_p5_s_bal", "Fine-tune P>=5 dB, S-balanced matched"), |
| ("finetune_p10_s_bal", "Fine-tune P>=10 dB, S-balanced matched"), |
| ("scratch_full", "Scratch full matched"), |
| ("scratch_p5_s_bal", "Scratch P>=5 dB, S-balanced matched"), |
| ("scratch_p10_s_bal", "Scratch P>=10 dB, S-balanced matched"), |
| ] |
| else: |
| phase_labels = [ |
| ("full", "Full matched"), |
| ("p5_s_bal", "P>=5 dB, S-balanced matched"), |
| ("p10_s_bal", "P>=10 dB, S-balanced matched"), |
| ] |
| else: |
| phase_labels = [ |
| ("full", "Full matched"), |
| ("snr5", "SNR>5 dB matched"), |
| ("snr10", "SNR>10 dB matched"), |
| ] |
| for slug, label in phase_labels: |
| label = condition_label(all_rows, "phase", slug, label) |
| mf = lookup(all_rows, "phase", slug, "mean_f1") |
| pf = lookup(all_rows, "phase", slug, "P_f1") |
| sf = lookup(all_rows, "phase", slug, "S_f1") |
| lines.append( |
| f"| {label} | {mf['mean']:.3f} +/- {mf['std']:.3f} | " |
| f"{pf['mean']:.3f} +/- {pf['std']:.3f} | {sf['mean']:.3f} +/- {sf['std']:.3f} |" |
| ) |
| lines.append("") |
|
|
| if has_task(all_rows, "dispersion"): |
| lines.extend([ |
| "", |
| "## Dispersion Estimation", |
| "", |
| "| Training subset | MAE (km/s) | RMSE (km/s) |", |
| "|---|---:|---:|", |
| ]) |
| for slug, label in [("full", "Full matched"), ("snr_q1", "SNR>3.04 dB matched"), ("snr_q2", "SNR>6.77 dB matched")]: |
| mae = lookup(all_rows, "dispersion", slug, "val_mae") |
| rmse = lookup(all_rows, "dispersion", slug, "val_rmse") |
| lines.append( |
| f"| {label} | {mae['mean']:.4f} +/- {mae['std']:.4f} | " |
| f"{rmse['mean']:.4f} +/- {rmse['std']:.4f} |" |
| ) |
| (out_dir / "multiseed_summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8") |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--seeds", nargs="+", type=int, default=[20260609, 20260610, 20260611]) |
| parser.add_argument("--out-dir", default="outputs/grl_multiseed_seed20260609_20260611") |
| parser.add_argument("--tasks", nargs="+", choices=["phase", "dispersion"], default=["phase", "dispersion"]) |
| parser.add_argument( |
| "--phase-balanced", |
| action="store_true", |
| help="Aggregate phase-aware P/S-balanced SNR outputs for the phase task.", |
| ) |
| parser.add_argument( |
| "--phase-prefix", |
| default=None, |
| help="Override the phase output-directory prefix before the seed, e.g. snr_transfer_phase_complete_seed.", |
| ) |
| args = parser.parse_args() |
|
|
| out_dir = ROOT / args.out_dir |
| all_rows: list[dict] = [] |
| metadata: dict[str, dict] = {} |
| if "phase" in args.tasks: |
| rows, meta = aggregate_task( |
| "phase", |
| args.seeds, |
| PHASE_METRICS, |
| out_dir, |
| phase_balanced=args.phase_balanced, |
| phase_prefix=args.phase_prefix, |
| ) |
| all_rows.extend(rows) |
| metadata["phase"] = meta |
| if "dispersion" in args.tasks: |
| rows, meta = aggregate_task("dispersion", args.seeds, DISP_METRICS, out_dir) |
| all_rows.extend(rows) |
| metadata["dispersion"] = meta |
|
|
| with (out_dir / "multiseed_summary.json").open("w", encoding="utf-8") as f: |
| json.dump({"metadata": metadata, "rows": all_rows}, f, indent=2) |
| write_markdown(all_rows, out_dir, phase_balanced=args.phase_balanced) |
| print((out_dir / "multiseed_summary.md").read_text(encoding="utf-8")) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|