snr_bias / code /scripts /grl_aggregate_multiseed.py
cangyeone's picture
Upload GRL reproducibility package
7170296 verified
Raw
History Blame Contribute Delete
9.27 kB
#!/usr/bin/env python3
"""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()