| |
| """Generate GRL revision figures from bootstrap and SNR-stratified tables.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import csv |
| from pathlib import Path |
|
|
| import matplotlib.pyplot as plt |
| import numpy as np |
|
|
|
|
| DEFAULT_REVISION_DIR = Path("outputs/grl_revision_20260610") |
| DEFAULT_FIG_DIR = Path("grl_overleaf/figures/grl_revision") |
|
|
|
|
| def read_rows(path: Path) -> list[dict[str, str]]: |
| if not path.exists(): |
| raise FileNotFoundError(f"Missing required table: {path}") |
| with path.open(newline="", encoding="utf-8") as f: |
| return list(csv.DictReader(f)) |
|
|
|
|
| def style(ax): |
| ax.spines["top"].set_visible(False) |
| ax.spines["right"].set_visible(False) |
| ax.grid(axis="y", color="#e6e6e6") |
| ax.set_axisbelow(True) |
|
|
|
|
| def lookup(rows: list[dict[str, str]], condition: str, metric: str) -> dict[str, float]: |
| row = next(r for r in rows if r["condition"] == condition and r["metric"] == metric) |
| return {k: float(row[k]) for k in ("estimate", "ci_low", "ci_high")} |
|
|
|
|
| def fig2(revision_dir: Path, fig_dir: Path) -> None: |
| phase = read_rows(revision_dir / "tables" / "phase_bootstrap_ci.csv") |
| disp = read_rows(revision_dir / "tables" / "dispersion_bootstrap_ci.csv") |
| fig, axs = plt.subplots(1, 2, figsize=(7.2, 3.1), constrained_layout=True) |
| conds = ["full", "snr5", "snr10"] |
| labels = ["Full", "SNR>5", "SNR>10"] |
| x = np.arange(len(conds)) |
| ax = axs[0] |
| for metric, color, marker in [("P_f1", "#4477AA", "o"), ("S_f1", "#CC6677", "s"), ("mean_f1", "#228833", "^")]: |
| vals = [lookup(phase, c, metric) for c in conds] |
| y = np.array([v["estimate"] for v in vals]) |
| yerr = np.array([[v["estimate"] - v["ci_low"] for v in vals], [v["ci_high"] - v["estimate"] for v in vals]]) |
| ax.errorbar(x, y, yerr=yerr, marker=marker, linewidth=2, capsize=3, label=metric.replace("_", " "), color=color) |
| ax.set_xticks(x, labels) |
| ax.set_ylabel("F1") |
| ax.set_title("Phase picking") |
| ax.legend(frameon=False, fontsize=8) |
| style(ax) |
|
|
| ax = axs[1] |
| conds_d = ["full", "snr_q1", "snr_q2"] |
| labels_d = ["Full", "SNR>3.04", "SNR>6.77"] |
| x = np.arange(len(conds_d)) |
| for metric, color, marker in [("mae", "#4477AA", "o"), ("rmse", "#CC6677", "s")]: |
| vals = [lookup(disp, c, metric) for c in conds_d] |
| y = np.array([v["estimate"] for v in vals]) |
| yerr = np.array([[v["estimate"] - v["ci_low"] for v in vals], [v["ci_high"] - v["estimate"] for v in vals]]) |
| ax.errorbar(x, y, yerr=yerr, marker=marker, linewidth=2, capsize=3, label=metric.upper(), color=color) |
| ax.set_xticks(x, labels_d) |
| ax.set_ylabel("Velocity error (km/s)") |
| ax.set_title("Dispersion") |
| ax.legend(frameon=False, fontsize=8) |
| style(ax) |
| fig_dir.mkdir(parents=True, exist_ok=True) |
| for ext in ("pdf", "png"): |
| fig.savefig(fig_dir / f"fig2_bootstrap_ci.{ext}", dpi=300, bbox_inches="tight") |
| plt.close(fig) |
|
|
|
|
| def si_stratified(revision_dir: Path, fig_dir: Path) -> None: |
| phase = read_rows(revision_dir / "tables" / "phase_snr_stratified_metrics.csv") |
| disp = read_rows(revision_dir / "tables" / "dispersion_snr_stratified_metrics.csv") |
| fig, axs = plt.subplots(1, 2, figsize=(7.2, 3.1), constrained_layout=True) |
| ax = axs[0] |
| for condition, color in [("full", "#4477AA"), ("snr5", "#CC6677"), ("snr10", "#228833")]: |
| rows = [r for r in phase if r["condition"] == condition and r["phase"] == "P_S_mean"] |
| rows.sort(key=lambda r: r["test_snr_bin"]) |
| ax.plot(range(len(rows)), [float(r["f1"]) for r in rows], marker="o", label=condition, color=color) |
| ax.set_xticks(range(len({r["test_snr_bin"] for r in phase}))) |
| ax.set_xticklabels(sorted({r["test_snr_bin"] for r in phase}), rotation=25, ha="right", fontsize=7) |
| ax.set_ylabel("Mean F1") |
| ax.set_title("Phase picking by test SNR") |
| ax.legend(frameon=False, fontsize=8) |
| style(ax) |
|
|
| ax = axs[1] |
| for condition, color in [("full", "#4477AA"), ("snr_q1", "#CC6677"), ("snr_q2", "#228833")]: |
| rows = [r for r in disp if r["condition"] == condition] |
| rows.sort(key=lambda r: r["test_snr_bin"]) |
| ax.plot(range(len(rows)), [float(r["mae"]) for r in rows], marker="o", label=condition, color=color) |
| ax.set_xticks(range(len({r["test_snr_bin"] for r in disp}))) |
| ax.set_xticklabels(sorted({r["test_snr_bin"] for r in disp}), rotation=25, ha="right", fontsize=7) |
| ax.set_ylabel("MAE (km/s)") |
| ax.set_title("Dispersion by test SNR") |
| ax.legend(frameon=False, fontsize=8) |
| style(ax) |
| fig_dir.mkdir(parents=True, exist_ok=True) |
| for ext in ("pdf", "png"): |
| fig.savefig(fig_dir / f"figS_snr_stratified.{ext}", dpi=300, bbox_inches="tight") |
| plt.close(fig) |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description=__doc__) |
| parser.add_argument("--revision-dir", type=Path, default=DEFAULT_REVISION_DIR) |
| parser.add_argument("--fig-dir", type=Path, default=DEFAULT_FIG_DIR) |
| args = parser.parse_args() |
| fig2(args.revision_dir, args.fig_dir) |
| si_stratified(args.revision_dir, args.fig_dir) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|