# SPDX-License-Identifier: Apache-2.0
# GeoPrior-v3 — https://github.com/earthai-tech/geoprior-v3
# Copyright (c) 2026-present
# Author: LKouadio <https://lkouadio.com>
r"""Extra plotting helpers for uncertainty diagnostics."""
from __future__ import annotations
import argparse
import glob
from pathlib import Path
from typing import Any
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from . import config as cfg
from . import utils
# -------------------------------------------------------------------
# Small helpers
# -------------------------------------------------------------------
def _smart_glob(
path_or_glob: str | None,
) -> Path | None:
"""
Resolve a path or glob.
If wildcards are present, we pick the most recently
modified match to reduce ambiguity when multiple runs
exist under results/.
"""
if not path_or_glob:
return None
s = str(path_or_glob).strip()
if not s:
return None
if any(ch in s for ch in "*?[]"):
cands = glob.glob(s, recursive=True)
if not cands:
return None
cands.sort(
key=lambda p: Path(p).stat().st_mtime,
reverse=True,
)
return Path(cands[0]).expanduser()
p = Path(s).expanduser()
return p if p.exists() else None
def _extract_factors_per_horizon(
meta: dict[str, Any] | None,
) -> list[float] | None:
"""
Robustly extract horizon scale factors from GeoPrior JSON.
Expected:
interval_calibration.factors_per_horizon
Accepts:
- list[float]
- dict like {"H1": 1.1, "H2": 0.9, ...}
"""
if not meta:
return None
ic = meta.get("interval_calibration") or {}
fac = ic.get("factors_per_horizon", None)
if fac is None:
return None
if isinstance(fac, list):
out = []
for v in fac:
try:
out.append(float(v))
except Exception:
continue
return out or None
if isinstance(fac, dict):
items: list[tuple[int, float]] = []
for k, v in fac.items():
ks = str(k).strip().lower()
if ks.startswith("h"):
ks = ks[1:]
try:
hi = int(ks)
fv = float(v)
except Exception:
continue
items.append((hi, fv))
if not items:
return None
items.sort(key=lambda t: t[0])
return [fv for _, fv in items]
return None
# -------------------------------------------------------------------
# Core uncertainty helpers
# -------------------------------------------------------------------
def _coverage_sharpness_by_horizon(
df: pd.DataFrame,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
horizons: list[int] = []
cov: list[float] = []
shp: list[float] = []
for h, g in df.groupby("forecast_step", sort=True):
horizons.append(int(h))
q10 = g["subsidence_q10"].to_numpy()
q90 = g["subsidence_q90"].to_numpy()
y = g["subsidence_actual"].to_numpy()
cov.append(float(np.mean((y >= q10) & (y <= q90))))
shp.append(float(np.mean(q90 - q10)))
H = np.asarray(horizons, dtype=int)
idx = np.argsort(H)
return (
H[idx],
np.asarray(cov, dtype=float)[idx],
np.asarray(shp, dtype=float)[idx],
)
def _bootstrap_ci(
x: np.ndarray,
func,
*,
B: int,
alpha: float = 0.05,
rng: np.random.Generator | None = None,
) -> tuple[float, float]:
rng = rng or np.random.default_rng(42)
x = np.asarray(x, dtype=float).ravel()
n = x.size
if n == 0:
return (np.nan, np.nan)
stats = np.empty(int(B), dtype=float)
for b in range(int(B)):
idx = rng.integers(0, n, n)
stats[b] = func(x[idx])
lo, hi = np.quantile(
stats,
[alpha / 2.0, 1.0 - alpha / 2.0],
)
return (float(lo), float(hi))
def _empirical_cdf_ks(u: np.ndarray) -> tuple[float, float]:
"""
KS statistic vs Uniform(0,1), with asymptotic p-value.
p ≈ 2 Σ (-1)^{k-1} exp(-2 k² n D²), k=1..100
"""
u = np.sort(np.asarray(u, dtype=float).ravel())
n = u.size
if n == 0:
return (np.nan, np.nan)
ecdf = np.arange(1, n + 1, dtype=float) / n
diff1 = ecdf - u
diff2 = u - (np.arange(n, dtype=float) / n)
D = float(np.max(np.maximum(diff1, diff2)))
s = 0.0
for k in range(1, 101):
s += ((-1) ** (k - 1)) * np.exp(
-2.0 * (k * k) * n * (D * D)
)
p = max(0.0, min(1.0, 2.0 * s))
return (D, p)
def _pit_from_three_quantiles(
q10: np.ndarray,
q50: np.ndarray,
q90: np.ndarray,
y: np.ndarray,
) -> np.ndarray:
"""
Approximate PIT using a 3-point piecewise-linear CDF.
CDF anchors:
(q10 -> 0.1), (q50 -> 0.5), (q90 -> 0.9)
with linear extrapolation and clamped tails.
"""
q10 = np.asarray(q10, dtype=float).ravel()
q50 = np.asarray(q50, dtype=float).ravel()
q90 = np.asarray(q90, dtype=float).ravel()
y = np.asarray(y, dtype=float).ravel()
eps = 1e-8
m1 = (0.5 - 0.1) / np.maximum(q50 - q10, eps)
m2 = (0.9 - 0.5) / np.maximum(q90 - q50, eps)
pit = np.empty_like(y, dtype=float)
for i in range(y.size):
if y[i] <= q50[i]:
p = 0.1 + m1[i] * (y[i] - q10[i])
else:
p = 0.5 + m2[i] * (y[i] - q50[i])
pit[i] = min(max(float(p), 0.0), 1.0)
return pit
def _reliability_points(
df: pd.DataFrame,
taus: list[float],
qcmap: dict[float, str],
) -> list[tuple[float, float]]:
y = df["subsidence_actual"].to_numpy()
pts: list[tuple[float, float]] = []
for t in taus:
q = df[qcmap[t]].to_numpy()
pts.append((t, float(np.mean(y <= q))))
return pts
def _quantile_residuals(
df: pd.DataFrame,
taus: list[float],
qcmap: dict[float, str],
) -> dict[float, np.ndarray]:
out: dict[float, np.ndarray] = {}
for t in taus:
res: list[float] = []
for _, g in df.groupby("forecast_step", sort=True):
emp = np.mean(
g["subsidence_actual"].to_numpy()
<= g[qcmap[t]].to_numpy()
)
res.append(float(emp - t))
out[t] = np.asarray(res, dtype=float)
return out
# -------------------------------------------------------------------
# Per-city summary
# -------------------------------------------------------------------
[docs]
def summarize_city(
df: pd.DataFrame,
*,
city: str,
B: int,
taus: tuple[float, float, float] = (0.1, 0.5, 0.9),
) -> dict[str, Any]:
qc = {
0.1: "subsidence_q10",
0.5: "subsidence_q50",
0.9: "subsidence_q90",
}
H, cov, shp = _coverage_sharpness_by_horizon(df)
cov_ci: list[tuple[float, float]] = []
shp_ci: list[tuple[float, float]] = []
rng = np.random.default_rng(42)
for h in H:
g = df[df["forecast_step"] == h]
q10 = g["subsidence_q10"].to_numpy()
q90 = g["subsidence_q90"].to_numpy()
y = g["subsidence_actual"].to_numpy()
cov_vec = ((y >= q10) & (y <= q90)).astype(float)
width = (q90 - q10).astype(float)
cov_ci.append(
_bootstrap_ci(
cov_vec,
np.mean,
B=B,
rng=rng,
)
)
shp_ci.append(
_bootstrap_ci(
width,
np.mean,
B=B,
rng=rng,
)
)
rel = {
int(h): _reliability_points(
df[df["forecast_step"] == h],
list(taus),
qc,
)
for h in H
}
pit = _pit_from_three_quantiles(
df["subsidence_q10"],
df["subsidence_q50"],
df["subsidence_q90"],
df["subsidence_actual"],
)
ksD, ksp = _empirical_cdf_ks(pit)
qres = _quantile_residuals(df, list(taus), qc)
return {
"city": city,
"horizons": H,
"coverage80": cov,
"coverage80_ci": np.asarray(cov_ci, dtype=float),
"sharpness80": shp,
"sharpness80_ci": np.asarray(shp_ci, dtype=float),
"reliability": rel,
"pit": pit,
"ks_D": ksD,
"ks_p": ksp,
"qresiduals": qres,
}
# -------------------------------------------------------------------
# Plotting
# -------------------------------------------------------------------
[docs]
def plot_s5(
*,
nansha: dict[str, Any] | None,
zhongshan: dict[str, Any] | None,
factors_n: list[float] | None,
factors_z: list[float] | None,
out_stem: Path,
dpi: int,
show_legends: bool,
show_labels: bool,
show_ticklabels: bool,
show_title: bool,
show_panel_titles: bool,
title: str | None,
) -> None:
utils.set_paper_style(dpi=int(dpi))
horizons: list[np.ndarray] = []
if nansha is not None:
horizons.append(nansha["horizons"])
if zhongshan is not None:
horizons.append(zhongshan["horizons"])
if not horizons:
raise SystemExit("No city data to plot.")
H = np.unique(np.concatenate(horizons))
nH = int(len(H))
fig = plt.figure(figsize=(10.2, 8.6), dpi=dpi)
gs = fig.add_gridspec(
nrows=4,
ncols=max(nH, 3),
height_ratios=[1.2, 1.0, 1.0, 1.0],
hspace=0.65,
wspace=0.35,
)
taus = [0.1, 0.5, 0.9]
# A) Reliability small multiples
for i, h in enumerate(H):
ax = fig.add_subplot(gs[0, i])
ax.plot(
[0.0, 1.0],
[0.0, 1.0],
"--",
color="#888888",
lw=1.0,
label="Ideal",
)
def _one(city: dict[str, Any] | None, name: str):
if city is None:
return
pts = city["reliability"].get(int(h), [])
if not pts:
return
xs = [p[0] for p in pts]
ys = [p[1] for p in pts]
ax.plot(
xs,
ys,
marker="o",
lw=1.0,
color=cfg.CITY_COLORS[name],
label=name,
)
_one(nansha, "Nansha")
_one(zhongshan, "Zhongshan")
ax.set_xlim(0.0, 1.0)
ax.set_ylim(0.0, 1.0)
if show_ticklabels:
ax.set_xticks(taus)
ax.set_yticks([0.0, 0.5, 1.0])
else:
ax.set_xticks([])
ax.set_yticks([])
if show_panel_titles:
ax.set_title(
f"Reliability • H{int(h)}",
loc="left",
fontweight="bold",
)
if i == 0 and show_labels:
ax.set_ylabel("Empirical quantile")
if i == nH - 1 and show_legends:
ax.legend(
frameon=False,
loc="lower right",
)
# B) PIT histograms (2 panels)
ax_pit_n = fig.add_subplot(gs[1, 0])
if nansha is not None:
ax_pit_n.hist(
nansha["pit"],
bins=20,
range=(0.0, 1.0),
color=cfg.CITY_COLORS["Nansha"],
alpha=0.9,
edgecolor="white",
)
if show_panel_titles:
ax_pit_n.set_title(
"PIT • Nansha "
f"(KS={nansha['ks_D']:.2f}, "
f"p≈{nansha['ks_p']:.2f})",
loc="left",
fontweight="bold",
)
else:
ax_pit_n.set_axis_off()
ax_pit_z = fig.add_subplot(gs[1, 1])
if zhongshan is not None:
ax_pit_z.hist(
zhongshan["pit"],
bins=20,
range=(0.0, 1.0),
color=cfg.CITY_COLORS["Zhongshan"],
alpha=0.9,
edgecolor="white",
)
if show_panel_titles:
ax_pit_z.set_title(
"PIT • Zhongshan "
f"(KS={zhongshan['ks_D']:.2f}, "
f"p≈{zhongshan['ks_p']:.2f})",
loc="left",
fontweight="bold",
)
else:
ax_pit_z.set_axis_off()
for ax in (ax_pit_n, ax_pit_z):
if not show_ticklabels:
ax.set_xticks([])
ax.set_yticks([])
else:
ax.set_xlim(0.0, 1.0)
if show_labels:
ax.set_xlabel("PIT")
ax.set_ylabel("Count")
else:
ax.set_xlabel("")
ax.set_ylabel("")
# C) Coverage vs horizon
ax_cov = fig.add_subplot(gs[1, max(2, nH - 1) :])
for city, name in (
(nansha, "Nansha"),
(zhongshan, "Zhongshan"),
):
if city is None:
continue
Hc = city["horizons"]
cov = city["coverage80"]
ci = city["coverage80_ci"]
ax_cov.plot(
Hc,
cov,
marker="o",
color=cfg.CITY_COLORS[name],
label=name,
)
ax_cov.fill_between(
Hc,
ci[:, 0],
ci[:, 1],
color=cfg.CITY_COLORS[name],
alpha=0.15,
)
ax_cov.axhline(
0.8,
ls="--",
lw=1.0,
color="#888888",
label="Target 80%",
)
if show_panel_titles:
ax_cov.set_title(
"Coverage (80%) vs horizon",
loc="left",
fontweight="bold",
)
if show_labels:
ax_cov.set_xlabel("Horizon (years)")
ax_cov.set_ylabel("Coverage (prop.)")
else:
ax_cov.set_xlabel("")
ax_cov.set_ylabel("")
if not show_ticklabels:
ax_cov.set_xticks([])
ax_cov.set_yticks([])
else:
ax_cov.set_ylim(0.0, 1.0)
if show_legends:
ax_cov.legend(frameon=False, loc="lower right")
# D) Sharpness vs horizon
ax_shp = fig.add_subplot(gs[2, 0:2])
for city, name in (
(nansha, "Nansha"),
(zhongshan, "Zhongshan"),
):
if city is None:
continue
Hc = city["horizons"]
shp = city["sharpness80"]
ci = city["sharpness80_ci"]
ax_shp.plot(
Hc,
shp,
marker="o",
color=cfg.CITY_COLORS[name],
label=name,
)
ax_shp.fill_between(
Hc,
ci[:, 0],
ci[:, 1],
color=cfg.CITY_COLORS[name],
alpha=0.15,
)
if show_panel_titles:
ax_shp.set_title(
"Sharpness (80% width) vs horizon",
loc="left",
fontweight="bold",
)
if show_labels:
ax_shp.set_xlabel("Horizon (years)")
ax_shp.set_ylabel("Width (units)")
else:
ax_shp.set_xlabel("")
ax_shp.set_ylabel("")
if not show_ticklabels:
ax_shp.set_xticks([])
ax_shp.set_yticks([])
if show_legends:
ax_shp.legend(frameon=False, loc="upper left")
# E) Calibration factors (optional)
ax_fac = fig.add_subplot(gs[2, max(2, nH - 1) :])
have_fac = (factors_n is not None) or (
factors_z is not None
)
if have_fac:
x = np.arange(nH, dtype=float)
w = 0.35
if factors_n is not None:
ax_fac.bar(
x - w / 2.0,
factors_n[:nH],
width=w,
color=cfg.CITY_COLORS["Nansha"],
edgecolor="white",
label="Nansha",
)
if factors_z is not None:
ax_fac.bar(
x + w / 2.0,
factors_z[:nH],
width=w,
color=cfg.CITY_COLORS["Zhongshan"],
edgecolor="white",
label="Zhongshan",
)
if show_ticklabels:
ax_fac.set_xticks(x)
ax_fac.set_xticklabels([f"H{int(h)}" for h in H])
else:
ax_fac.set_xticks([])
ax_fac.set_yticks([])
if show_panel_titles:
ax_fac.set_title(
"Calibration factors (per horizon)",
loc="left",
fontweight="bold",
)
if show_labels:
ax_fac.set_ylabel("Scale factor")
else:
ax_fac.set_ylabel("")
if show_legends:
ax_fac.legend(frameon=False)
else:
ax_fac.text(
0.5,
0.5,
"No calibration factors found",
ha="center",
va="center",
transform=ax_fac.transAxes,
)
ax_fac.set_axis_off()
# F) Quantile residuals vs horizon
ax_qr = fig.add_subplot(gs[3, :])
for t, style in zip(
[0.1, 0.5, 0.9], ["--", "-", "--"], strict=False
):
if nansha is not None:
ax_qr.plot(
nansha["horizons"],
nansha["qresiduals"][t],
linestyle=style,
marker="o",
color=cfg.CITY_COLORS["Nansha"],
label=f"Nansha Δ({t:.1f})",
)
if zhongshan is not None:
ax_qr.plot(
zhongshan["horizons"],
zhongshan["qresiduals"][t],
linestyle=style,
marker="s",
color=cfg.CITY_COLORS["Zhongshan"],
label=f"Zhongshan Δ({t:.1f})",
)
ax_qr.axhline(0.0, color="#888888", lw=1.0)
if show_panel_titles:
ax_qr.set_title(
"Quantile residuals by horizon",
loc="left",
fontweight="bold",
)
if show_labels:
ax_qr.set_xlabel("Horizon (years)")
ax_qr.set_ylabel("Residual")
else:
ax_qr.set_xlabel("")
ax_qr.set_ylabel("")
if not show_ticklabels:
ax_qr.set_xticks([])
ax_qr.set_yticks([])
if show_legends:
ax_qr.legend(
frameon=False, ncol=3, loc="upper center"
)
if show_title:
fig.suptitle(
utils.resolve_title(
default=(
"Supplementary Fig. S5 • "
"Expanded uncertainty diagnostics"
),
title=title,
),
y=0.995,
fontweight="bold",
)
utils.save_figure(fig, str(out_stem), dpi=int(dpi))
# fig.savefig(str(out_stem) + ".png", bbox_inches="tight")
# fig.savefig(str(out_stem) + ".pdf", bbox_inches="tight")
plt.close(fig)
# -------------------------------------------------------------------
# Tables
# -------------------------------------------------------------------
[docs]
def write_tables(
*,
nansha: dict[str, Any] | None,
zhongshan: dict[str, Any] | None,
out_csv: Path,
out_tex: Path,
) -> None:
rows: list[dict[str, Any]] = []
for city in (nansha, zhongshan):
if city is None:
continue
for i, h in enumerate(city["horizons"]):
rows.append(
{
"city": city["city"],
"horizon": int(h),
"coverage80": float(
city["coverage80"][i]
),
"coverage80_lo": float(
city["coverage80_ci"][i, 0]
),
"coverage80_hi": float(
city["coverage80_ci"][i, 1]
),
"sharpness80": float(
city["sharpness80"][i]
),
"sharpness80_lo": float(
city["sharpness80_ci"][i, 0]
),
"sharpness80_hi": float(
city["sharpness80_ci"][i, 1]
),
"ks_D": float(city["ks_D"]),
"ks_p": float(city["ks_p"]),
}
)
df = pd.DataFrame(rows)
utils.ensure_dir(out_csv.parent)
df.to_csv(out_csv, index=False)
utils.ensure_dir(out_tex.parent)
with out_tex.open("w", encoding="utf-8") as f:
f.write("\\begin{table}[t]\\centering\\small\n")
f.write(
"\\begin{tabular}{l r r r r r r r r r}\\toprule\n"
)
f.write(
"City & H & Cov$_{80}$ & CI$_{lo}$ & "
"CI$_{hi}$ & Sharp$_{80}$ & CI$_{lo}$ & "
"CI$_{hi}$ & KS $D$ & KS $p$ \\\\\n"
)
f.write("\\midrule\n")
for _, r in df.iterrows():
f.write(
f"{r['city']} & {int(r['horizon'])} & "
f"{r['coverage80']:.3f} & "
f"{r['coverage80_lo']:.3f} & "
f"{r['coverage80_hi']:.3f} & "
f"{r['sharpness80']:.3f} & "
f"{r['sharpness80_lo']:.3f} & "
f"{r['sharpness80_hi']:.3f} & "
f"{r['ks_D']:.3f} & "
f"{r['ks_p']:.3f} \\\\\n"
)
f.write("\\bottomrule\\end{tabular}\n")
f.write(
"\\caption{Expanded uncertainty diagnostics "
"supporting Fig. 5.}\n"
)
f.write("\\label{tab:s5_uncertainty}\\end{table}\n")
# -------------------------------------------------------------------
# Artifact resolution (new structure)
# -------------------------------------------------------------------
def _pick_forecast_from_src(
src: Any,
*,
split: str,
) -> Path | None:
art = utils.detect_artifacts(src)
if split == "val":
return art.forecast_val_csv
if split == "test":
return art.forecast_test_csv
if art.forecast_test_csv is not None:
return art.forecast_test_csv
return art.forecast_val_csv
def _pick_phys_json_from_src(src: Any) -> Path | None:
# Prefer interpretable JSON when available.
p = utils.find_phys_json(src)
if p is not None:
return p
art = utils.detect_artifacts(src)
return art.phys_json
# -------------------------------------------------------------------
# CLI (new API)
# -------------------------------------------------------------------
[docs]
def parse_args(
argv: list[str] | None = None,
*,
prog: str | None = None,
) -> argparse.Namespace:
p = argparse.ArgumentParser(
prog=prog or "plot-uncertainty-extras",
description=(
"Build Supplementary Fig. S5 + reliability "
"table from calibrated forecasts."
),
)
utils.add_city_flags(p, default_both=True)
p.add_argument(
"--ns-src",
type=str,
default=None,
help="Nansha run dir (auto-discover inputs).",
)
p.add_argument(
"--zh-src",
type=str,
default=None,
help="Zhongshan run dir (auto-discover inputs).",
)
p.add_argument(
"--ns-forecast",
type=str,
default=None,
help="Override Nansha calibrated forecast CSV.",
)
p.add_argument(
"--zh-forecast",
type=str,
default=None,
help="Override Zhongshan calibrated forecast CSV.",
)
p.add_argument(
"--ns-phys-json",
type=str,
default=None,
help="Override Nansha GeoPrior phys JSON.",
)
p.add_argument(
"--zh-phys-json",
type=str,
default=None,
help="Override Zhongshan GeoPrior phys JSON.",
)
p.add_argument(
"--split",
type=str,
choices=["auto", "val", "test"],
default="auto",
help="When using --*-src, pick val/test files.",
)
p.add_argument(
"--bootstrap",
type=int,
default=1000,
help="Bootstrap draws for CIs.",
)
p.add_argument(
"--dpi",
type=int,
default=cfg.PAPER_DPI,
help="Figure dpi.",
)
# Text toggles (Nature workflow)
utils.add_plot_text_args(
p,
default_out="supp-fig-s5-uncertainty-extras",
)
# Backward-friendly switch
p.add_argument(
"--no-legends",
action="store_true",
help="Alias for --show-legend=false",
)
p.add_argument(
"--tables-stem",
type=str,
default="supp-table-s5-reliability",
help="Output stem for CSV/TEX (scripts/out/).",
)
return p.parse_args(argv)
[docs]
def supp_figS5_uncertainty_extras_main(
argv: list[str] | None = None,
*,
prog: str | None = None,
) -> None:
args = parse_args(argv, prog=prog)
utils.ensure_script_dirs()
cities = utils.resolve_cities(args)
if not cities:
cities = ["Nansha", "Zhongshan"]
want_ns = "Nansha" in cities
want_zh = "Zhongshan" in cities
show_leg = utils.str_to_bool(
args.show_legend, default=True
)
show_lbl = utils.str_to_bool(
args.show_labels, default=True
)
show_tkl = utils.str_to_bool(
args.show_ticklabels, default=True
)
show_tit = utils.str_to_bool(
args.show_title, default=True
)
show_pt = utils.str_to_bool(
args.show_panel_titles,
default=True,
)
if args.no_legends:
show_leg = False
# Resolve Nansha inputs
df_n = None
fac_n = None
if want_ns:
fp = _smart_glob(args.ns_forecast)
jp = _smart_glob(args.ns_phys_json)
if fp is None and args.ns_src:
fp = _pick_forecast_from_src(
args.ns_src, split=args.split
)
if jp is None and args.ns_src:
jp = _pick_phys_json_from_src(args.ns_src)
if fp is not None:
df_n = utils.load_forecast_csv(fp)
jmeta = utils.safe_load_json(jp)
fac_n = _extract_factors_per_horizon(jmeta)
# Resolve Zhongshan inputs
df_z = None
fac_z = None
if want_zh:
fp = _smart_glob(args.zh_forecast)
jp = _smart_glob(args.zh_phys_json)
if fp is None and args.zh_src:
fp = _pick_forecast_from_src(
args.zh_src, split=args.split
)
if jp is None and args.zh_src:
jp = _pick_phys_json_from_src(args.zh_src)
if fp is not None:
df_z = utils.load_forecast_csv(fp)
jmeta = utils.safe_load_json(jp)
fac_z = _extract_factors_per_horizon(jmeta)
nansha = (
summarize_city(df_n, city="Nansha", B=args.bootstrap)
if df_n is not None
else None
)
zhongshan = (
summarize_city(
df_z, city="Zhongshan", B=args.bootstrap
)
if df_z is not None
else None
)
# Outputs
fig_stem = utils.resolve_fig_out(args.out)
if fig_stem.suffix:
fig_stem = fig_stem.with_suffix("")
tab_stem = utils.resolve_out_out(args.tables_stem)
if tab_stem.suffix:
tab_stem = tab_stem.with_suffix("")
plot_s5(
nansha=nansha,
zhongshan=zhongshan,
factors_n=fac_n,
factors_z=fac_z,
out_stem=fig_stem,
dpi=int(args.dpi),
show_legends=bool(show_leg),
show_labels=bool(show_lbl),
show_ticklabels=bool(show_tkl),
show_title=bool(show_tit),
show_panel_titles=bool(show_pt),
title=args.title,
)
write_tables(
nansha=nansha,
zhongshan=zhongshan,
out_csv=Path(str(tab_stem) + ".csv"),
out_tex=Path(str(tab_stem) + ".tex"),
)
print(f"[OK] fig : {fig_stem}.png/.pdf")
print(f"[OK] table: {tab_stem}.csv/.tex")
[docs]
def main(
argv: list[str] | None = None,
*,
prog: str | None = None,
) -> None:
supp_figS5_uncertainty_extras_main(argv, prog=prog)
if __name__ == "__main__":
main()
# Example runs
# Auto-discover from run folders:
# python -m scripts supp-fig-s5-uncertainty-extras \
# --ns-src results/nansha_stage2_run \
# --zh-src results/zhongshan_stage2_run \
# --split auto
# Override CSVs directly:
# python -m scripts supp-fig-s5-uncertainty-extras \
# --ns-forecast "results/**/nansha_*calibrated.csv" \
# --zh-forecast "results/**/zhongshan_*calibrated.csv" \
# --no-legends