| |
| """Create the matched SNR filtering design figure for the GRL manuscript.""" |
|
|
| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import math |
| from pathlib import Path |
|
|
| import matplotlib.pyplot as plt |
|
|
|
|
| ROOT = Path(__file__).resolve().parents[1] |
| PHASE_SUMMARY = ROOT / "outputs" / "snr_transfer_seed20260609" / "summary.json" |
| DISP_SUMMARY = ROOT / "outputs" / "disp_snr_transfer_seed20260609" / "summary.json" |
| DISP_SNR_CACHE = ROOT / "outputs" / "disp_snr_transfer_seed20260609" / "ncf_snr_cache.json" |
| DEFAULT_OUTPUT = ROOT / "grl_overleaf" / "figures" / "snr_matched_design.png" |
|
|
|
|
| def read_json(path: Path) -> dict: |
| with path.open("r", encoding="utf-8") as f: |
| return json.load(f) |
|
|
|
|
| def dispersion_candidate_counts(summary: dict) -> list[int]: |
| cache = read_json(DISP_SNR_CACHE) |
| q1 = summary["snr_thresholds"]["q1"] |
| q2 = summary["snr_thresholds"]["q2"] |
| train_snr = [ |
| item["snr_db"] |
| for item in cache.values() |
| if item.get("split") == "train" and math.isfinite(float(item.get("snr_db", float("nan")))) |
| ] |
| return [ |
| len(train_snr), |
| sum(float(value) > q1 for value in train_snr), |
| sum(float(value) > q2 for value in train_snr), |
| ] |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) |
| args = parser.parse_args() |
|
|
| phase = read_json(PHASE_SUMMARY) |
| disp = read_json(DISP_SUMMARY) |
|
|
| phase_candidate = [ |
| phase["candidate_train_records"]["full"], |
| phase["candidate_train_records"]["snr5"], |
| phase["candidate_train_records"]["snr10"], |
| ] |
| phase_matched = [row["train_records"] for row in phase["rows"]] |
| disp_matched = [row["train_records"] for row in disp["rows"]] |
| disp_candidate = dispersion_candidate_counts(disp) |
|
|
| labels = ["Full", "SNR > 5", "SNR > 10"] |
| disp_labels = ["Full", "SNR > 3.04", "SNR > 6.77"] |
| colors = ["#2f6f73", "#c47f2b", "#8f4d6b"] |
|
|
| fig, axes = plt.subplots(1, 2, figsize=(10.8, 4.6), dpi=220) |
| panels = [ |
| (axes[0], "A", "Phase picking", labels, phase_candidate, phase_matched, "records"), |
| (axes[1], "B", "Dispersion", disp_labels, disp_candidate, disp_matched, "samples"), |
| ] |
| for ax, panel_label, title, xlabels, candidate, matched, unit in panels: |
| x = range(len(xlabels)) |
| ax.bar(x, candidate, width=0.62, color="#d7d7d7", edgecolor="#555555", label="candidate pool") |
| ax.bar(x, matched, width=0.38, color=colors, edgecolor="#222222", label="matched training") |
| for idx, value in enumerate(candidate): |
| ax.text(idx, value * 1.02, f"{value:,}", ha="center", va="bottom", fontsize=7) |
| for idx, value in enumerate(matched): |
| ax.text(idx, value * 0.50, f"{value:,}", ha="center", va="center", fontsize=8, color="white") |
| ax.set_title(title, fontsize=11) |
| ax.set_xticks(list(x)) |
| ax.set_xticklabels(xlabels, fontsize=8) |
| ax.set_ylabel(f"training {unit}") |
| ax.spines[["top", "right"]].set_visible(False) |
| ax.grid(axis="y", color="#eeeeee", linewidth=0.8) |
| ax.set_axisbelow(True) |
| ax.text( |
| -0.12, |
| 1.08, |
| panel_label, |
| transform=ax.transAxes, |
| fontsize=13, |
| fontweight="bold", |
| va="top", |
| ) |
| axes[0].legend(frameon=False, loc="upper right", fontsize=8) |
| fig.suptitle("Matched training budgets isolate the effect of SNR filtering", fontsize=13, y=0.98) |
| fig.tight_layout(rect=(0, 0, 1, 0.94)) |
|
|
| args.output.parent.mkdir(parents=True, exist_ok=True) |
| fig.savefig(args.output, bbox_inches="tight") |
| print(f"Wrote {args.output}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|