07 — Facet Comparison: Blocking vs Propagating Anticyclones (PRP)

Compares PV-budget grouped-term decomposition between blocking and propagating anticyclone (PRP) events using bootstrap significance testing and orthogonal basis projection.

Config options:

  • LEVEL: "wavg" for pre-computed wavg fields, or an integer hPa (e.g. 250) for a single isobaric level from the _3d arrays.

  • When LEVEL="wavg": LHS tendency = pv_anom_dt + pv_bar_dt, RHS terms use 2-D wavg fields.

  • When LEVEL=<int>: LHS tendency = pv_dt_3d[k], RHS terms use _3d[k] arrays directly.

Data:

  • Blocking: /net/flood/data2/users/x_yan/composite_blocking_tempest

  • PRP: /net/flood/data2/users/x_yan/composite_prp_tempest

[1]:
import numpy as np
import matplotlib.pyplot as plt
import os, glob
from concurrent.futures import ThreadPoolExecutor
from zipfile import BadZipFile

from pvtend import compute_orthogonal_basis, project_field
from pvtend.plotting import plot_var, plot_baroclinic_tilt
from pvtend.decomposition.smoothing import gaussian_smooth_nan

1 Configuration

[2]:

# ═══════════════════════════════════════════════════════════ # Configuration # ═══════════════════════════════════════════════════════════ DATA_ROOTS = { "blocking": "/net/flood/data2/users/x_yan/pvtend/outputs/blocking", "prp": "/net/flood/data2/users/x_yan/pvtend/outputs/prp", } STAGE = "peak" DH = 0 DH_BASIS = max(DH - 1, -13) SMOOTH_DEG = 3.0 # 3° Gaussian smoothing GRID_SP = 1.5 N_BOOT = 200 ALPHA = 0.05 SEED = 42 # Level: "wavg" for pre-computed weighted-average, or int hPa (e.g. 250) LEVEL = 250 #"wavg # ═══════════════════════════════════════════════════════════ # Projection masking config (toggle on/off) # ═══════════════════════════════════════════════════════════ USE_SIG_MASK = True # zero out non-significant pixels before projection MASK_SPEC = "< 0" # restrict basis to q' < 0 region (mask) # ═══════════════════════════════════════════════════════════ # I/O helpers # ═══════════════════════════════════════════════════════════ def _load_npz(path): try: return dict(np.load(path)) except (BadZipFile, EOFError, OSError): return None def load_events(etype, dh, stage=STAGE): """Load all events for an event type at a given dh.""" sign = "+" if dh >= 0 else "" d = f"{DATA_ROOTS[etype]}/{stage}/dh={sign}{dh}" files = sorted(glob.glob(os.path.join(d, "track_*.npz"))) if not files: return [] with ThreadPoolExecutor(max_workers=8) as pool: results = list(pool.map(_load_npz, files)) good = [r for r in results if r is not None] n_bad = len(results) - len(good) if n_bad: print(f" ⚠ {etype} dh={dh}: skipped {n_bad} corrupt NPZ files") return good def get_field(event, name, level=LEVEL): """Extract a 2-D field, respecting LEVEL config. If LEVEL="wavg", returns the pre-computed 2-D wavg field. If LEVEL=<int hPa>, slices the _3d array at that level. """ if isinstance(level, str) and level.lower() == "wavg": return event[name] else: key_3d = name if name.endswith("_3d") else name + "_3d" arr_3d = event[key_3d] lvls = event["levels"] idx = int(np.argmin(np.abs(lvls - float(level)))) return arr_3d[idx] def get_lhs_tendency(event, level=LEVEL): """Return full-field dq/dt (LHS tendency). wavg: pv_dt 3D: pv_dt_3d[k] (full field exists) """ if isinstance(level, str) and level.lower() == "wavg": return event["pv_dt"] else: lvls = event["levels"] idx = int(np.argmin(np.abs(lvls - float(level)))) return event["pv_dt_3d"][idx] smooth = lambda f: gaussian_smooth_nan(f, smoothing_deg=SMOOTH_DEG, grid_spacing=GRID_SP) print(f"Stage={STAGE} DH={DH} DH_BASIS={DH_BASIS} " f"LEVEL={LEVEL} Smooth={SMOOTH_DEG}° N_BOOT={N_BOOT}") print(f" USE_SIG_MASK={USE_SIG_MASK} USE_NEG_PV_MASK={MASK_SPEC}")
Stage=peak  DH=0  DH_BASIS=-1  LEVEL=250  Smooth=3.0°  N_BOOT=200
  USE_SIG_MASK=True  USE_NEG_PV_MASK=< 0

2 Load events & build composite basis

[3]:
# ── Load events for both types ──
events = {}
events_basis = {}
for etype in DATA_ROOTS:
    events[etype] = load_events(etype, DH)
    events_basis[etype] = load_events(etype, DH_BASIS)
    print(f"{etype}: N={len(events[etype])} (dh={DH}), "
          f"N_basis={len(events_basis[etype])} (dh={DH_BASIS})")

# Grid coordinates (same for both types)
X_rel = events["blocking"][0]["X_rel"]
Y_rel = events["blocking"][0]["Y_rel"]
x_rel = X_rel[0, :]
y_rel = Y_rel[:, 0]

# ── Build composite-mean basis from dh-1 for each type ──
basis = {}
for etype in DATA_ROOTS:
    # evs_b = events_basis[etype]
    # pv_b  = np.nanmean([e["pv_anom"] for e in evs_b], axis=0)
    # dx_b  = np.nanmean([e["pv_dx"] for e in evs_b], axis=0)
    # dy_b  = np.nanmean([e["pv_dy"] for e in evs_b], axis=0)

    # dh composite means (current-time positional fields)
    evs = events[etype]
    pv_n = np.nanmean([e["pv_anom"] for e in evs], axis=0)
    dx_n = np.nanmean([e["pv_dx"] for e in evs], axis=0)
    dy_n = np.nanmean([e["pv_dy"] for e in evs], axis=0)
    dx_dx_n = np.nanmean([e["pv_dx_dx"] for e in evs], axis=0)
    dy_dy_n = np.nanmean([e["pv_dy_dy"] for e in evs], axis=0)
    dx_dy_n = np.nanmean([e["pv_dx_dy"] for e in evs], axis=0)

    basis[etype] = compute_orthogonal_basis(
        pv_n, dx_n, dy_n, x_rel, y_rel,
        mask=MASK_SPEC,
        apply_smoothing=True, smoothing_deg=SMOOTH_DEG, grid_spacing=GRID_SP,
        # pv_anom_prev=pv_b, pv_dx_prev=dx_b, pv_dy_prev=dy_b,
        pv_dx_dx_prev=dx_dx_n, pv_dy_dy_prev=dy_dy_n, pv_dx_dy_prev=dx_dy_n
    )
    print(f"{etype} basis norms: "
          + " ".join(f"{k}={v:.3e}" for k, v in basis[etype].norms.items()))

blocking: N=1260 (dh=0), N_basis=1260 (dh=-1)
prp: N=2097 (dh=0), N_basis=2096 (dh=-1)
blocking basis norms: beta=2.699e+02 ax=2.959e+02 ay=1.653e+02 gamma1=1.120e+02 gamma2=6.998e+01 sigma=4.141e+01
prp basis norms: beta=1.975e+02 ax=1.749e+02 ay=1.061e+02 gamma1=5.176e+01 gamma2=5.184e+01 sigma=1.812e+01
/tmp/ipykernel_1373389/2842660874.py:33: UserWarning: compute_orthogonal_basis: grid_spacing=1.5°, center_lat=60.0°N → dx(center)=83.4 km, dy=166.8 km
  basis[etype] = compute_orthogonal_basis(

3 Bootstrap helpers

[4]:
def bootstrap_field(events_list, field_func, n_boot=N_BOOT, alpha=ALPHA, seed=SEED):
    """Bootstrap composite mean + significance mask for a field.

    Returns (mean_2d, sig_mask) where sig_mask=True for significant pixels.
    """
    stack = np.array([field_func(e) for e in events_list])  # (N, Y, X)
    N = stack.shape[0]
    rng = np.random.default_rng(seed)

    boot = np.empty((n_boot, *stack.shape[1:]))
    for b in range(n_boot):
        idx = rng.integers(0, N, size=N)
        boot[b] = np.nanmean(stack[idx], axis=0)

    lo = np.nanpercentile(boot, 100 * alpha / 2, axis=0)
    hi = np.nanpercentile(boot, 100 * (1 - alpha / 2), axis=0)
    mean = np.nanmean(stack, axis=0)
    sig_mask = ~((lo <= 0) & (hi >= 0))
    return mean, sig_mask


def bootstrap_project(events_list, field_func, basis_obj,
                      n_boot=N_BOOT, seed=SEED):
    """Bootstrap the projected coefficients (sig-only field).

    Per-event: zero out non-sig pixels, smooth, project.
    Bootstrap the mean coefficients.
    Returns dict with keys beta, ax, ay, gamma1, gamma2, sigma — each (n_boot,).
    """
    # First get composite sig mask
    _, sig_mask = bootstrap_field(events_list, field_func)

    # Per-event projections using sig-only composite field
    per_event = []
    for e in events_list:
        raw = field_func(e)
        if USE_SIG_MASK:
            sig_field = np.where(sig_mask, raw, 0.0)
        else:
            sig_field = raw.copy()
        sig_s = smooth(sig_field)
        p = project_field(sig_s, basis_obj)
        per_event.append({k: p[k] for k in ["beta", "ax", "ay", "gamma1", "gamma2", "sigma"]})

    per_arr = {k: np.array([pe[k] for pe in per_event])
               for k in ["beta", "ax", "ay", "gamma1", "gamma2", "sigma"]}
    N = len(events_list)
    rng = np.random.default_rng(seed)

    boot = {k: np.empty(n_boot) for k in per_arr}
    for b in range(n_boot):
        idx = rng.integers(0, N, size=N)
        for k in boot:
            boot[k][b] = per_arr[k][idx].mean()

    return boot


print("Bootstrap helpers defined.")
Bootstrap helpers defined.

4 Define grouped terms for cell 6 (4-group) and cell 7 (7-term)

[5]:
# ═══════════════════════════════════════════════════════════
#  4-GROUP decomposition (Cell 6 bar plot)
# ═══════════════════════════════════════════════════════════

GROUPS_4 = {
    r"$-$Lead. adv": lambda e: -(
        get_field(e, "u_rot_bar_pv_anom_dx") + get_field(e, "v_rot_anom_pv_bar_dy")
    ),
    r"$-$Eddy rot": lambda e: -(
        get_field(e, "u_rot_anom_pv_anom_dx") + get_field(e, "v_rot_anom_pv_anom_dy")
    ),
    r"$-u_{\chi, \text{adiabatic}} \nabla q'$": lambda e: -(
        get_field(e, "u_div_adiabatic_pv_anom_dx") + get_field(e, "v_div_adiabatic_pv_anom_dy")
    ),
    r"$-u_{\chi, \text{diabatic}} \nabla q'$": lambda e: -(
        get_field(e, "u_div_diabatic_pv_anom_dx") + get_field(e, "v_div_diabatic_pv_anom_dy")
    ),
}

# ═══════════════════════════════════════════════════════════
#  7-TERM decomposition (Cell 7 bar plot)
# ═══════════════════════════════════════════════════════════

GROUPS_7 = {
    r"$-\bar{u}_\Psi\,q'_x$":           lambda e: -get_field(e, "u_rot_bar_pv_anom_dx"),
    r"$-v'_\Psi\,\bar{q}_y$":           lambda e: -get_field(e, "v_rot_anom_pv_bar_dy"),
    r"$-$Eddy rot":                lambda e: -(
        get_field(e, "u_rot_anom_pv_anom_dx") + get_field(e, "v_rot_anom_pv_anom_dy")
    ),
    r"$-$Eddy adiabatic div":            lambda e: -(
        get_field(e, "u_div_adiabatic_pv_anom_dx") + get_field(e, "v_div_adiabatic_pv_anom_dy")
    ),
    r"$-$Eddy diabatic div":            lambda e: -(
        get_field(e, "u_div_diabatic_pv_anom_dx") + get_field(e, "v_div_diabatic_pv_anom_dy")
    ),
    r"$-\omega\,\bar{q}_p$":      lambda e: -(
        get_field(e, "w_adiabatic_pv_bar_dp") + get_field(e, "w_diabatic_pv_bar_dp")
    ),
    r"$Q$": lambda e: (
        get_field(e, "Q")
    ),
}


print(f"4-group terms: {list(GROUPS_4.keys())}")
print(f"7-term  terms: {list(GROUPS_7.keys())}")
4-group terms: ['$-$Lead. adv', '$-$Eddy rot', "$-u_{\\chi, \\text{adiabatic}} \\nabla q'$", "$-u_{\\chi, \\text{diabatic}} \\nabla q'$"]
7-term  terms: ["$-\\bar{u}_\\Psi\\,q'_x$", "$-v'_\\Psi\\,\\bar{q}_y$", '$-$Eddy rot', '$-$Eddy adiabatic div', '$-$Eddy diabatic div', '$-\\omega\\,\\bar{q}_p$', '$Q$']

5 Run bootstrap for all terms (both event types)

[6]:
# Also bootstrap the LHS tendency (pv_dt)
ALL_TERMS = {"dq/dt": get_lhs_tendency}
ALL_TERMS.update(GROUPS_4)

print("═══ Bootstrap for 4-group decomposition ═══")
boot_4 = {}  # boot_4[etype][term_name] = {beta: (N_BOOT,), ...}
for etype in DATA_ROOTS:
    boot_4[etype] = {}
    evs = events[etype]
    b = basis[etype]
    for tname, tfunc in ALL_TERMS.items():
        boot_4[etype][tname] = bootstrap_project(evs, tfunc, b)
        lo, hi = np.nanpercentile(boot_4[etype][tname]["ax"], [2.5, 97.5])
        sig = "***" if lo * hi > 0 else "n.s."
        print(f"  {etype:10s} {tname:35s}  αx 95%CI: [{lo:.3e}, {hi:.3e}]  {sig}")

print("\n═══ Bootstrap for 7-term decomposition ═══")
boot_7 = {}
ALL_TERMS_7 = {"dq/dt": get_lhs_tendency}
ALL_TERMS_7.update(GROUPS_7)
for etype in DATA_ROOTS:
    boot_7[etype] = {}
    evs = events[etype]
    b = basis[etype]
    for tname, tfunc in ALL_TERMS_7.items():
        boot_7[etype][tname] = bootstrap_project(evs, tfunc, b)
        lo, hi = np.nanpercentile(boot_7[etype][tname]["ax"], [2.5, 97.5])
        sig = "***" if lo * hi > 0 else "n.s."
        print(f"  {etype:10s} {tname:35s}  αx 95%CI: [{lo:.3e}, {hi:.3e}]  {sig}")

print("\nDone.")
═══ Bootstrap for 4-group decomposition ═══
  blocking   dq/dt                                αx 95%CI: [1.787e-01, 3.937e-01]  ***
  blocking   $-$Lead. adv                         αx 95%CI: [6.164e+00, 7.690e+00]  ***
  blocking   $-$Eddy rot                          αx 95%CI: [-3.305e-01, -5.820e-02]  ***
  blocking   $-u_{\chi, \text{adiabatic}} \nabla q'$  αx 95%CI: [-7.527e-01, -5.935e-01]  ***
  blocking   $-u_{\chi, \text{diabatic}} \nabla q'$  αx 95%CI: [-8.040e-01, -5.852e-01]  ***
  prp        dq/dt                                αx 95%CI: [1.309e+00, 1.830e+00]  ***
  prp        $-$Lead. adv                         αx 95%CI: [8.970e+00, 1.154e+01]  ***
  prp        $-$Eddy rot                          αx 95%CI: [4.993e-01, 9.183e-01]  ***
  prp        $-u_{\chi, \text{adiabatic}} \nabla q'$  αx 95%CI: [-8.538e-01, -6.429e-01]  ***
  prp        $-u_{\chi, \text{diabatic}} \nabla q'$  αx 95%CI: [-1.121e+00, -7.810e-01]  ***

═══ Bootstrap for 7-term decomposition ═══
  blocking   dq/dt                                αx 95%CI: [1.787e-01, 3.937e-01]  ***
  blocking   $-\bar{u}_\Psi\,q'_x$                αx 95%CI: [1.783e+01, 2.035e+01]  ***
  blocking   $-v'_\Psi\,\bar{q}_y$                αx 95%CI: [-1.181e+01, -1.052e+01]  ***
  blocking   $-$Eddy rot                          αx 95%CI: [-3.305e-01, -5.820e-02]  ***
  blocking   $-$Eddy adiabatic div                αx 95%CI: [-7.527e-01, -5.935e-01]  ***
  blocking   $-$Eddy diabatic div                 αx 95%CI: [-8.040e-01, -5.852e-01]  ***
  blocking   $-\omega\,\bar{q}_p$                 αx 95%CI: [-4.151e+00, -3.458e+00]  ***
  blocking   $Q$                                  αx 95%CI: [-6.404e-02, -4.759e-02]  ***
  prp        dq/dt                                αx 95%CI: [1.309e+00, 1.830e+00]  ***
  prp        $-\bar{u}_\Psi\,q'_x$                αx 95%CI: [2.206e+01, 2.637e+01]  ***
  prp        $-v'_\Psi\,\bar{q}_y$                αx 95%CI: [-1.438e+01, -1.226e+01]  ***
  prp        $-$Eddy rot                          αx 95%CI: [4.993e-01, 9.183e-01]  ***
  prp        $-$Eddy adiabatic div                αx 95%CI: [-8.538e-01, -6.429e-01]  ***
  prp        $-$Eddy diabatic div                 αx 95%CI: [-1.121e+00, -7.810e-01]  ***
  prp        $-\omega\,\bar{q}_p$                 αx 95%CI: [-6.217e+00, -5.053e+00]  ***
  prp        $Q$                                  αx 95%CI: [-1.202e-01, -8.168e-02]  ***

Done.

6 Bar plot — 4-group comparison (Blocking vs PRP)

[7]:
fig, axes = plt.subplots(2, 3, figsize=(16, 10))
coef_names = ["beta", "ax", "ay", "gamma1", "gamma2", "sigma"]
coef_labels = [r"$\beta$ (intensification) [s$^{-1}$]",
               r"$\alpha_x$ (zonal propagation) [m/s]",
               r"$\alpha_y$ (meridional propagation) [m/s]",
               r"$\gamma_1$ (shear def.) [km$^2$/s]",
               r"$\gamma_2$ (strain def.) [km$^2$/s]",
               r"$\sigma$ (Laplacian) [km$^2$/s]"]

term_names_4 = list(ALL_TERMS.keys())
n_terms = len(term_names_4)
x_pos = np.arange(n_terms)
width = 0.35
colors_etype = {"blocking": "steelblue", "prp": "coral"}

for ax, cname, clabel in zip(axes.flat, coef_names, coef_labels):
    bar_info = {}

    # ── Draw bars ──
    for i_et, etype in enumerate(["blocking", "prp"]):
        means, los, his, sigs = [], [], [], []
        for tname in term_names_4:
            vals = boot_4[etype][tname][cname]
            m = vals.mean()
            lo, hi = np.nanpercentile(vals, [2.5, 97.5])
            means.append(m)
            los.append(m - lo)
            his.append(hi - m)
            sigs.append(lo * hi > 0)
        bar_info[etype] = (means, los, his, sigs)

        offset = -width / 2 if i_et == 0 else width / 2
        ax.bar(x_pos + offset, means, width,
               yerr=[los, his], capsize=4,
               label=etype.capitalize(),
               color=colors_etype[etype], alpha=0.85)

    # ── Set y-limits from actual bar extents (not forced 0-centered) ──
    ymin_all, ymax_all = 0, 0
    for etype in ["blocking", "prp"]:
        ms, ls, hs, _ = bar_info[etype]
        for m, l, h in zip(ms, ls, hs):
            ymax_all = max(ymax_all, m + h)
            ymin_all = min(ymin_all, m - l)
    pad = (ymax_all - ymin_all) * 0.15
    ax.set_ylim(ymin_all - pad, ymax_all + pad)

    # ── Significance stars ──
    ylim = ax.get_ylim()
    for i_et, etype in enumerate(["blocking", "prp"]):
        means, los, his, sigs = bar_info[etype]
        offset = -width / 2 if i_et == 0 else width / 2
        for j, (s, m) in enumerate(zip(sigs, means)):
            if s:
                y_off = his[j] if m >= 0 else -los[j]
                y_star = m + y_off * 1.1 + np.sign(m) * abs(m) * 0.05
                y_star = np.clip(y_star, ylim[0] * 0.95, ylim[1] * 0.95)
                ax.text(x_pos[j] + offset, y_star,
                        "***", ha="center", fontsize=7, color="red",
                        fontweight="bold", clip_on=True)

    # ── Between-group significance: blocking − PRP ──
    yr = ylim[1] - ylim[0]
    for j, tname in enumerate(term_names_4):
        diff = boot_4["blocking"][tname][cname] - boot_4["prp"][tname][cname]
        lo_d, hi_d = np.nanpercentile(diff, [2.5, 97.5])
        if lo_d * hi_d > 0:
            top_j = max(
                bar_info["blocking"][0][j] + bar_info["blocking"][2][j],
                bar_info["prp"][0][j] + bar_info["prp"][2][j])
            bot_j = min(
                bar_info["blocking"][0][j] - bar_info["blocking"][1][j],
                bar_info["prp"][0][j] - bar_info["prp"][1][j])
            if abs(top_j) >= abs(bot_j):
                ym = min(top_j + yr * 0.05, ylim[1] - yr * 0.01)
            else:
                ym = max(bot_j - yr * 0.05, ylim[0] + yr * 0.01)
            ax.text(x_pos[j], ym, "†",
                    ha="center", fontsize=11, color="black", fontweight="bold")

    ax.set_xticks(x_pos)
    ax.set_xticklabels([t.replace("\n", " ") for t in term_names_4], fontsize=8, rotation=15, ha="right")
    ax.axhline(0, color="k", lw=0.5, ls="--")
    ax.set_ylabel(clabel, fontsize=9)
    if ax is axes.flat[0]:
        ax.legend(fontsize=9)

level_str = f"wavg" if isinstance(LEVEL, str) else f"{LEVEL} hPa"
fig.text(0.5, -0.02,
         r"Red *** = significantly $\neq$ 0  (p < 0.05)     "
         r"Black † = blocking vs PRP difference significant  (p < 0.05)",
         ha="center", fontsize=9, style="italic")
fig.suptitle(
    f"4-Group Decomposition — Blocking vs PRP\n"
    f"{STAGE} dh={DH}  Level={level_str}  Smooth={SMOOTH_DEG}°  "
    f"N_block={len(events['blocking'])}  N_prp={len(events['prp'])}  B={N_BOOT}",
    fontsize=12, fontweight="bold", y=1.02)
fig.tight_layout()
plt.show()
../_images/notebooks_07_facet_blocking_vs_prp_13_0.png

7 Bar plot — 7-term comparison (Blocking vs PRP)

[8]:

fig, axes = plt.subplots(2, 2, figsize=(18, 18)) term_names_7 = list(ALL_TERMS_7.keys()) n_terms_7 = len(term_names_7) x_pos_7 = np.arange(n_terms_7) for ax, cname, clabel in zip(axes.flat, coef_names, coef_labels): bar_info = {} # etype -> (means, lo_err, hi_err, sig_neq0) # ── Draw bars ── for i_et, etype in enumerate(["blocking", "prp"]): means, los, his, sigs = [], [], [], [] for tname in term_names_7: vals = boot_7[etype][tname][cname] m = vals.mean() lo, hi = np.nanpercentile(vals, [2.5, 97.5]) means.append(m) los.append(m - lo) his.append(hi - m) sigs.append(lo * hi > 0) bar_info[etype] = (means, los, his, sigs) offset = -width / 2 if i_et == 0 else width / 2 ax.bar(x_pos_7 + offset, means, width, yerr=[los, his], capsize=3, label=etype.capitalize(), color=colors_etype[etype], alpha=0.85) # Red *** = CI excludes 0 (significantly != 0) for j, (s, m) in enumerate(zip(sigs, means)): if s: y_off = his[j] if m >= 0 else -los[j] ax.text(x_pos_7[j] + offset, m + y_off * 1.1 + np.sign(m) * abs(m) * 0.05, "***", ha="center", fontsize=7, color="red", fontweight="bold") # ── Between-group significance: blocking − PRP ── ylim = ax.get_ylim() yr = ylim[1] - ylim[0] for j, tname in enumerate(term_names_7): diff = boot_7["blocking"][tname][cname] - boot_7["prp"][tname][cname] lo_d, hi_d = np.nanpercentile(diff, [2.5, 97.5]) if lo_d * hi_d > 0: # significant difference between groups top_j = max( bar_info["blocking"][0][j] + bar_info["blocking"][2][j], bar_info["prp"][0][j] + bar_info["prp"][2][j]) bot_j = min( bar_info["blocking"][0][j] - bar_info["blocking"][1][j], bar_info["prp"][0][j] - bar_info["prp"][1][j]) if abs(top_j) >= abs(bot_j): ym = min(top_j + yr * 0.05, ylim[1] - yr * 0.01) else: ym = max(bot_j - yr * 0.05, ylim[0] + yr * 0.01) ax.text(x_pos_7[j], ym, "†", ha="center", fontsize=11, color="black", fontweight="bold") ax.set_xticks(x_pos_7) ax.set_xticklabels(term_names_7, fontsize=7, rotation=20, ha="right") ax.axhline(0, color="k", lw=0.5, ls="--") ax.set_ylabel(clabel, fontsize=9) ax.set_yscale("symlog") if ax is axes.flat[0]: ax.legend(fontsize=9) fig.text(0.5, -0.02, r"Red *** = significantly $\neq$ 0 (p < 0.05) " r"Black † = blocking vs PRP difference significant (p < 0.05)", ha="center", fontsize=9, style="italic") fig.suptitle( f"7-Term Decomposition — Blocking vs PRP\n" f"{STAGE} dh={DH} Level={level_str} Smooth={SMOOTH_DEG}° " f"N_block={len(events['blocking'])} N_prp={len(events['prp'])} B={N_BOOT}", fontsize=12, fontweight="bold", y=1.02) fig.tight_layout() plt.show()
../_images/notebooks_07_facet_blocking_vs_prp_15_0.png
[10]:

# ── αx-only bar plot (7-term) — CI bars only, no significance markers ── fig, ax = plt.subplots(figsize=(10, 5)) cname = "ax" clabel = r"$\alpha_x$ (zonal propagation) [m/s]" for i_et, etype in enumerate(["blocking", "prp"]): means, los, his = [], [], [] for tname in term_names_7: vals = boot_7[etype][tname][cname] m = vals.mean() lo, hi = np.nanpercentile(vals, [2.5, 97.5]) means.append(m) los.append(m - lo) his.append(hi - m) offset = -width / 2 if i_et == 0 else width / 2 ax.bar(x_pos_7 + offset, means, width, yerr=[los, his], capsize=4, label=etype.capitalize(), color=colors_etype[etype], alpha=0.85) ax.set_xticks(x_pos_7) ax.set_xticklabels(term_names_7, fontsize=9, rotation=20, ha="right") ax.axhline(0, color="k", lw=0.5, ls="--") ax.set_ylabel(clabel, fontsize=11) ax.set_yscale("symlog") ax.legend(fontsize=10) ax.set_title( f"$\\alpha_x$ (zonal propagation) — 7-Term Blocking vs PRP\n" f"{STAGE} dh={DH} Level={level_str} Smooth={SMOOTH_DEG}° " f"N_block={len(events['blocking'])} N_prp={len(events['prp'])} B={N_BOOT}", fontsize=11, fontweight="bold") fig.tight_layout() plt.show()
../_images/notebooks_07_facet_blocking_vs_prp_16_0.png
[11]:

# ── αx from composite-mean pkl files (no bootstrap, no CI) ── import pickle PKL_PATHS = { "blocking": "/net/flood/data2/users/x_yan/pvtend/outputs/blocking/composite_blocking.pkl", "prp": "/net/flood/data2/users/x_yan/pvtend/outputs/prp/composite_prp.pkl", } # Load composites composites = {} for etype, path in PKL_PATHS.items(): with open(path, "rb") as f: composites[etype] = pickle.load(f) print(f"{etype}: N={composites[etype]._pick('original', STAGE, DH)[2]}") # Helper: extract 2-D composite-mean field from pkl def comp_field(comp, name, stage=STAGE, dh=DH, level=LEVEL): key = name if name.endswith("_3d") else name + "_3d" return comp.reduce_2d(key, stage, dh, level_mode=level) # Build basis from composite means (same recipe as cell 6) basis_comp = {} for etype in PKL_PATHS: c = composites[etype] pv_n = comp_field(c, "pv_anom") dx_n = comp_field(c, "pv_dx") dy_n = comp_field(c, "pv_dy") dx_dx = comp_field(c, "pv_dx_dx") dy_dy = comp_field(c, "pv_dy_dy") dx_dy = comp_field(c, "pv_dx_dy") xr = c.x_rel[0, :] if c.x_rel.ndim == 2 else c.x_rel yr = c.y_rel[:, 0] if c.y_rel.ndim == 2 else c.y_rel basis_comp[etype] = compute_orthogonal_basis( pv_n, dx_n, dy_n, xr, yr, mask=MASK_SPEC, apply_smoothing=True, smoothing_deg=SMOOTH_DEG, grid_spacing=GRID_SP, pv_dx_dx_prev=dx_dx, pv_dy_dy_prev=dy_dy, pv_dx_dy_prev=dx_dy, ) print(f"{etype} composite basis norms: " + " ".join(f"{k}={v:.3e}" for k, v in basis_comp[etype].norms.items())) # 7-term composite fields and projection COMP_GROUPS_7 = { r"$-\bar{u}_\Psi\,q'_x$": lambda c: -comp_field(c, "u_rot_bar_pv_anom_dx"), r"$-v'_\Psi\,\bar{q}_y$": lambda c: -comp_field(c, "v_rot_anom_pv_bar_dy"), r"$-$Eddy rot": lambda c: -( comp_field(c, "u_rot_anom_pv_anom_dx") + comp_field(c, "v_rot_anom_pv_anom_dy")), r"$-$Eddy adiabatic div": lambda c: -( comp_field(c, "u_div_adiabatic_pv_anom_dx") + comp_field(c, "v_div_adiabatic_pv_anom_dy")), r"$-$Eddy diabatic div": lambda c: -( comp_field(c, "u_div_diabatic_pv_anom_dx") + comp_field(c, "v_div_diabatic_pv_anom_dy")), r"$-\omega\,\bar{q}_p$": lambda c: -( comp_field(c, "w_adiabatic_pv_bar_dp") + comp_field(c, "w_diabatic_pv_bar_dp")), r"$Q$": lambda c: comp_field(c, "Q"), } COMP_ALL_7 = {"dq/dt": lambda c: comp_field(c, "pv_dt")} COMP_ALL_7.update(COMP_GROUPS_7) # Project each composite-mean term comp_ax = {} # comp_ax[etype][tname] = scalar αx for etype in PKL_PATHS: comp_ax[etype] = {} c = composites[etype] b = basis_comp[etype] for tname, tfunc in COMP_ALL_7.items(): fld = smooth(tfunc(c)) p = project_field(fld, b) comp_ax[etype][tname] = p["ax"] # ── Plot ── fig, ax = plt.subplots(figsize=(10, 5)) comp_term_names = list(COMP_ALL_7.keys()) n_t = len(comp_term_names) x_p = np.arange(n_t) for i_et, etype in enumerate(["blocking", "prp"]): vals = [comp_ax[etype][t] for t in comp_term_names] offset = -width / 2 if i_et == 0 else width / 2 ax.bar(x_p + offset, vals, width, label=etype.capitalize(), color=colors_etype[etype], alpha=0.85) ax.set_xticks(x_p) ax.set_xticklabels(comp_term_names, fontsize=9, rotation=20, ha="right") ax.axhline(0, color="k", lw=0.5, ls="--") ax.set_ylabel(r"$\alpha_x$ (zonal propagation) [m/s]", fontsize=11) ax.set_yscale("symlog") ax.legend(fontsize=10) N_blk = composites["blocking"]._pick("original", STAGE, DH)[2] N_prp = composites["prp"]._pick("original", STAGE, DH)[2] ax.set_title( f"$\\alpha_x$ — Composite-mean projection (no bootstrap)\n" f"{STAGE} dh={DH} Level={level_str} Smooth={SMOOTH_DEG}° " f"N_block={N_blk} N_prp={N_prp}", fontsize=11, fontweight="bold") fig.tight_layout() plt.show()
blocking: N=1260
prp: N=2097
blocking composite basis norms: beta=2.831e+02 ax=2.733e+02 ay=1.639e+02 gamma1=9.718e+01 gamma2=6.578e+01 sigma=3.624e+01
prp composite basis norms: beta=1.998e+02 ax=1.678e+02 ay=9.868e+01 gamma1=4.714e+01 gamma2=4.432e+01 sigma=1.418e+01
/tmp/ipykernel_1373389/659284345.py:34: UserWarning: compute_orthogonal_basis: grid_spacing=1.5°, center_lat=60.0°N → dx(center)=83.4 km, dy=166.8 km
  basis_comp[etype] = compute_orthogonal_basis(
../_images/notebooks_07_facet_blocking_vs_prp_17_2.png