"""
=============================================================
SRL Supplemental Material S2 — Analysis Code
=============================================================
Title: Seismic Compression-Quiescence Anomalies as
       Earthquake Precursors: Prospective Prediction for
       the Kashmir Himalayan Fault Belt and Validation
       Across Four Tectonic Regimes

Author: Ramakrishna Pasupuleti
        Independent Researcher, Suryapet,
        Telangana 508213, India
        workisfun415@gmail.com
        ORCID: 0009-0008-8418-1430

Journal: Seismological Research Letters (SRL)
Manuscript: SRL-S-26-00341
Zenodo:  doi:10.5281/zenodo.20400117
         doi:10.5281/zenodo.20792073
Date:    June 2026

=============================================================
DESCRIPTION
=============================================================
This script reproduces all C, Q, and scoring calculations
reported in the manuscript. Given the ISC catalog CSV
(or any catalog with columns: time, latitude, longitude,
magnitude), it:

  1. Defines study zones (data-driven centers)
  2. Computes C and Q for each zone
  3. Identifies all historical C-drop cycles
  4. Computes six-criterion scores
  5. Prints a summary table matching Table 5 in the paper
  6. Generates all six manuscript figures

=============================================================
REQUIREMENTS
=============================================================
Python >= 3.9
  pip install pandas numpy scipy matplotlib

Input file (place in same folder as this script):
  india_ISC_M3plus_2000_2026.csv
  Columns required: time, latitude, longitude, magnitude
  Source: ISC Reviewed Bulletin
          doi:10.31905/D808B830

=============================================================
USAGE
=============================================================
  python SRL_SupplMat_S2_AnalysisCode.py

Output:
  zone_summary.csv   — Table 5 from paper
  cycle_table_Z21.csv — Table 4 from paper
  SRL_Fig1_Z21_Cycles.png
  SRL_Fig2_AllZones.png
  SRL_Fig3_ZoneMap.png
  SRL_Fig4_Z21_CurrentCycle.png
  SRL_Fig5_Z15b_Cycles.png
  SRL_Fig6_Z15_History.png

=============================================================
"""

# ── IMPORTS ──────────────────────────────────────────────────
import os
import sys
import warnings
import pandas as pd
import numpy as np
from scipy.ndimage import uniform_filter1d, gaussian_filter
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import matplotlib.dates as mdates
from matplotlib.patches import FancyBboxPatch
from matplotlib.lines import Line2D

warnings.filterwarnings('ignore')

print("="*60)
print("SRL Kashmir C-Drop Analysis")
print("Ramakrishna Pasupuleti, 2026")
print("="*60)

# ── PARAMETERS ───────────────────────────────────────────────

# Baseline period (2000–2009)
# A 10-year background window before the accelerated
# post-2010 earthquake sequences in Kashmir
BASE_START = pd.Timestamp('2000-01-01')
BASE_END   = pd.Timestamp('2009-12-31')

# Analysis end date
TODAY = pd.Timestamp('2026-06-20')

# Rolling window: W = 20 most recent earthquakes
# Chosen to give stable std while remaining responsive
W = 20

# Smoothing: 12-event moving average on C and Q series
SMOOTH = 12

# C-drop confirmation thresholds
C_PEAK_MIN  = 0.25   # minimum peak C for a valid cycle
C_DROP_MIN  = 0.050  # minimum drop from peak to confirm
C_DROP_DAYS = 180    # maximum days after peak

# Six-criterion thresholds
C1_THRESH = 0.25    # C >= 0.25 (compression active)
C2_THRESH = 0.20    # Q >= 0.20 (quiescence present)
C3_DROP   = 0.050   # C-drop >= 0.050 (PRIMARY signal)
C4_BVAL   = 0.75    # b-value < 0.75 (stress concentrated)
C4_NMIN   = 8       # minimum events for b-value
C5_NMAX   = 15      # < 15 events in last 90 days
C6_DAYS   = 180     # C peak within last 180 days

# Score threshold for CRITICAL classification
CRITICAL_SCORE = 3  # >= 3/6

# Validation window
VALIDATE_DAYS = 90  # days after C-drop onset

# ── ZONE DEFINITIONS ─────────────────────────────────────────
# Data-driven centers: placed at natural seismicity
# density peaks from ISC catalog density analysis.
# Format: (ID, fault, name, lat, lon, radius_km)

ZONES = [
    ('Z21',    'MCT-PJT', 'Anantnag',
     33.00, 75.00, 200),
    ('Z15',    'MBT',     'Muzaffarabad',
     34.00, 73.00, 200),
    ('Z15b',   'NPB',     'Kashmir North',
     35.00, 73.00, 200),
    ('FZ-NPB', 'MBT-NPB', 'Gilgit North',
     35.45, 74.75, 120),
    ('FZ-MCT', 'PJT',     'Ramban-Banihal',
     33.10, 75.35,  80),
]

# Known validation events (for figures)
# Format: (timestamp, magnitude, label)
VALIDATION_EVENTS = [
    (pd.Timestamp('2026-04-11'), 4.8, 'M4.8 Apr11'),
    (pd.Timestamp('2026-06-05'), 4.8, 'M4.8 Jun5'),
    (pd.Timestamp('2026-06-19'), 3.3, 'M3.3 Doda'),
]


# ── HELPER FUNCTIONS ─────────────────────────────────────────

def haversine(zlat, zlon, lats, lons):
    """
    Great-circle distance in kilometres.
    Uses the haversine formula.

    Parameters
    ----------
    zlat, zlon : float
        Zone center coordinates (degrees)
    lats, lons : array-like
        Event coordinates (degrees)

    Returns
    -------
    distances : np.ndarray
        Distances in km
    """
    R = 6371.0
    dlat = np.radians(lats - zlat)
    dlon = np.radians(lons - zlon)
    a = (np.sin(dlat / 2) ** 2
         + np.cos(np.radians(zlat))
         * np.cos(np.radians(lats))
         * np.sin(dlon / 2) ** 2)
    return R * 2 * np.arctan2(
        np.sqrt(a), np.sqrt(1 - a))


def compute_bvalue(mags, Mc=3.0):
    """
    Maximum likelihood estimate of Gutenberg-Richter
    b-value.

    b = log10(e) / (mean(M) - Mc)

    Parameters
    ----------
    mags : array-like
        Magnitudes
    Mc : float
        Completeness magnitude

    Returns
    -------
    b : float or np.nan
    """
    m = np.array(mags)
    m = m[m >= Mc]
    if len(m) < C4_NMIN:
        return np.nan
    mm = float(np.mean(m))
    if mm <= Mc:
        return np.nan
    return round(float(np.log10(np.e) / (mm - Mc)), 3)


def get_value(arr, tarr, ts):
    """
    Interpolate smoothed array at a given timestamp.

    Parameters
    ----------
    arr  : np.ndarray   — smoothed C or Q values
    tarr : DatetimeIndex
    ts   : Timestamp or str

    Returns
    -------
    value : float
    """
    t = pd.Timestamp(ts)
    if t > tarr[-1]:
        return round(float(arr[-1]), 4)
    if t < tarr[0]:
        return round(float(arr[0]), 4)
    idx = int(np.argmin(np.abs(tarr - t)))
    return round(float(arr[idx]), 4)


# ── DATA LOADING ─────────────────────────────────────────────

def load_catalog(filepath):
    """
    Load ISC-format catalog CSV.

    Required columns: time, latitude, longitude, magnitude
    Optional columns: depth

    Returns cleaned DataFrame sorted by time.
    """
    print(f"\nLoading catalog: {filepath}")
    if not os.path.exists(filepath):
        print(f"  ERROR: File not found: {filepath}")
        print("  Please download the ISC Reviewed Bulletin")
        print("  from doi:10.31905/D808B830 and place the")
        print("  CSV in the same folder as this script.")
        sys.exit(1)

    df = pd.read_csv(filepath, low_memory=False)
    df['time_n'] = pd.to_datetime(
        df['time'], utc=True, errors='coerce'
    ).dt.tz_localize(None)
    df = df.rename(columns={
        'latitude':  'lat',
        'longitude': 'lon',
        'magnitude': 'mag',
    })
    df = df.dropna(subset=['time_n', 'lat', 'lon', 'mag'])
    df = df[df.mag >= 3.0].sort_values(
        'time_n').reset_index(drop=True)

    print(f"  Loaded {len(df):,} events "
          f"(M >= 3.0)")
    print(f"  Period: {df.time_n.min().date()} "
          f"to {df.time_n.max().date()}")
    return df


# ── C AND Q COMPUTATION ───────────────────────────────────────

def compute_C_Q(zcat, sigma0, rate0):
    """
    Compute rolling C and Q for a zone catalog.

    C(k) = max(0, 1 - sigma(W_k) / sigma0)
    Q(k) = max(0, 1 - lambda_current / lambda0)

    where:
      sigma(W_k)       = std of W=20 most recent magnitudes
      sigma0           = baseline std (2000-2009)
      lambda_current   = W / (time span of window in years)
      lambda0          = baseline rate (events/year, 2000-2009)

    Parameters
    ----------
    zcat   : DataFrame with columns time_n, mag
    sigma0 : float — baseline standard deviation
    rate0  : float — baseline event rate (events/year)

    Returns
    -------
    Csm  : np.ndarray — smoothed C series
    Qsm  : np.ndarray — smoothed Q series
    Tarr : DatetimeIndex — timestamps
    """
    mags = zcat.mag.values
    Cv = []; Qv = []; Tv = []

    for k in range(W, len(zcat)):
        # Rolling window of W most recent magnitudes
        w = mags[k - W:k]

        # C: compression index
        # std(w) < sigma0 means magnitudes are compressed
        C = max(0.0, 1.0 - float(np.std(w)) / sigma0)

        tn = zcat.time_n.iloc[k]
        tp = zcat.time_n.iloc[k - W]

        # Current rate: W events in (tn - tp) days
        dt = max((tn - tp).days, 1)
        lam = W / (dt / 365.25)

        # Q: quiescence index
        # lam < rate0 means seismicity is below background
        Q = max(0.0, 1.0 - lam / rate0)

        Cv.append(C)
        Qv.append(Q)
        Tv.append(tn)

    # Apply 12-event moving average smoothing
    Csm = uniform_filter1d(np.array(Cv), size=SMOOTH)
    Qsm = uniform_filter1d(np.array(Qv), size=SMOOTH)
    Tarr = pd.DatetimeIndex(Tv)

    return Csm, Qsm, Tarr


def find_cdrop_cycles(Csm, Qsm, Tarr, zcat):
    """
    Identify all confirmed C-drop cycles.

    A cycle is defined as:
      1. C >= C_PEAK_MIN (0.25) at a local maximum
      2. C subsequently drops by at least C_DROP_MIN (0.05)
      3. The drop occurs within C_DROP_DAYS (180) days

    For each confirmed cycle, records whether a
    M >= 4.0 or M >= 4.5 earthquake followed within
    VALIDATE_DAYS (90) days of the peak.

    Parameters
    ----------
    Csm, Qsm : smoothed C and Q arrays
    Tarr     : DatetimeIndex
    zcat     : zone catalog DataFrame

    Returns
    -------
    cycles : list of dicts
    """
    cycles = []
    ci = 0

    while ci < len(Csm) - 10:
        cv = Csm[ci]

        if cv >= C_PEAK_MIN:
            # Check local maximum: higher than neighbors
            # within ±5 steps
            is_peak = (
                ci > 5
                and cv >= Csm[max(0, ci - 5)]
                and ci < len(Csm) - 5
                and cv >= Csm[ci + 5]
            )
            if is_peak:
                pk_C = cv
                pk_t = Tarr[ci]
                min_c = cv

                # Track minimum C after peak
                for j in range(ci + 1,
                               min(ci + 200,
                                   len(Csm))):
                    if Csm[j] < min_c:
                        min_c = Csm[j]

                    drop_j = pk_C - min_c
                    if drop_j >= C_DROP_MIN:
                        # Confirmed C-drop cycle
                        window_end = (pk_t
                            + pd.Timedelta(
                                days=VALIDATE_DAYS))
                        eqs = zcat[
                            (zcat.time_n >= pk_t)
                            & (zcat.time_n <= window_end)
                        ]
                        m40 = eqs[eqs.mag >= 4.0]
                        m45 = eqs[eqs.mag >= 4.5]

                        # Slice of C/Q for figure panels
                        i0 = max(0, ci - 20)
                        i1 = min(len(Csm), ci + 100)

                        cycles.append({
                            'pk_t':    pk_t,
                            'pkC':     round(pk_C, 4),
                            'drop':    round(drop_j, 4),
                            'm40':     len(m40) > 0,
                            'm45':     len(m45) > 0,
                            'maxm':    (round(float(
                                eqs.mag.max()), 1)
                                if len(eqs) > 0
                                else 0),
                            'Csm_slice': Csm[i0:i1],
                            'Tarr_slice': Tarr[i0:i1],
                        })

                        # Skip ahead past validation window
                        skip = window_end
                        while (ci < len(Tarr)
                               and Tarr[ci] < skip):
                            ci += 1
                        break

        ci += 1

    return cycles


def compute_score(c_now, q_now, pkC, drop,
                  days_pk, n90, b90):
    """
    Compute the six-criterion consensus score.

    C1: C >= 0.25            (compression active)
    C2: Q >= 0.20            (quiescence present)
    C3: drop >= 0.050        (C-drop confirmed — PRIMARY)
        from peak >= 0.25
    C4: b-value < 0.75       (stress concentrated)
        (only if n90 >= 8)
    C5: n90 < 15             (recent quiet)
    C6: peak within 180 days (fresh cycle)

    Parameters
    ----------
    c_now  : float — current C
    q_now  : float — current Q
    pkC    : float — cycle peak C
    drop   : float — C-drop so far
    days_pk: int   — days since peak
    n90    : int   — events in last 90 days
    b90    : float — b-value last 90 days

    Returns
    -------
    score : int (0–6)
    criteria : list of bool (6 elements)
    """
    sc1 = int(c_now >= C1_THRESH)
    sc2 = int(q_now >= C2_THRESH)
    sc3 = int(pkC >= C_PEAK_MIN
              and drop >= C3_DROP)
    sc4 = int(not np.isnan(b90)
              and b90 < C4_BVAL)
    sc5 = int(n90 < C5_NMAX)
    sc6 = int(days_pk <= C6_DAYS
              and pkC >= C_PEAK_MIN)

    return (sc1 + sc2 + sc3 + sc4 + sc5 + sc6,
            [sc1, sc2, sc3, sc4, sc5, sc6])


# ── MAIN ANALYSIS LOOP ────────────────────────────────────────

def analyse_all_zones(catalog_path):
    """
    Run the full analysis for all defined zones.

    Returns dict keyed by zone ID.
    """
    ind = load_catalog(catalog_path)

    results = {}

    for (zid, fault, zname, zlat, zlon, R) in ZONES:
        print(f"\nAnalysing {zid} — {zname} "
              f"({fault}, {zlat}N {zlon}E, "
              f"R={R}km)")

        # Select events inside zone
        dists = haversine(zlat, zlon,
                          ind.lat.values,
                          ind.lon.values)
        zcat = ind[dists <= R].sort_values(
            'time_n').reset_index(drop=True)

        if len(zcat) < W + 10:
            print(f"  SKIP: too few events ({len(zcat)})")
            continue

        # Baseline statistics (2000–2009)
        bm = ((zcat.time_n >= BASE_START)
              & (zcat.time_n <= BASE_END))
        bmags = zcat.mag.values[bm]
        if len(bmags) < 15:
            # Fall back to earliest 30 events
            bmags = zcat.mag.values[:30]

        sigma0 = max(float(np.std(bmags)), 0.05)
        n_base = len(zcat[
            (zcat.time_n >= BASE_START)
            & (zcat.time_n <= BASE_END)])
        base_years = (BASE_END - BASE_START).days / 365.25
        rate0 = max(n_base / base_years, 1.0)

        print(f"  Events: {len(zcat):,} | "
              f"sigma0={sigma0:.4f} | "
              f"rate0={rate0:.2f}/yr")

        # Compute C and Q
        Csm, Qsm, Tarr = compute_C_Q(
            zcat, sigma0, rate0)

        # Current values (as of TODAY)
        c_now = get_value(Csm, Tarr, TODAY)
        q_now = get_value(Qsm, Tarr, TODAY)

        # Peak C in last 365 days
        m365 = ((Tarr >= (TODAY
                  - pd.Timedelta(days=365)))
                & (Tarr <= TODAY))
        if m365.sum() > 3:
            pkC = float(np.max(Csm[m365]))
            pkT = Tarr[m365][
                int(np.argmax(Csm[m365]))]
        else:
            pkC = c_now
            pkT = TODAY

        drop = round(pkC - c_now, 4)
        days_pk = int((TODAY - pkT).days)

        # Events in last 90 days
        pre90 = zcat[
            (zcat.time_n >= (TODAY
                - pd.Timedelta(days=90)))
            & (zcat.time_n <= TODAY)]
        n90 = len(pre90)
        b90 = compute_bvalue(pre90.mag.values)

        # Six-criterion score
        score, criteria = compute_score(
            c_now, q_now, pkC, drop,
            days_pk, n90, b90)

        cdrop_confirmed = (
            pkC >= C_PEAK_MIN
            and drop >= C_DROP_MIN
            and days_pk <= C_DROP_DAYS)

        # Historical C-drop cycles
        cycles = find_cdrop_cycles(
            Csm, Qsm, Tarr, zcat)

        n_cyc = len(cycles)
        n40 = sum(1 for c in cycles if c['m40'])
        n45 = sum(1 for c in cycles if c['m45'])
        r40 = (round(n40 / n_cyc * 100, 1)
               if n_cyc > 0 else 0)
        r45 = (round(n45 / n_cyc * 100, 1)
               if n_cyc > 0 else 0)

        # Status label
        if cdrop_confirmed and score >= 5:
            status = 'CONFIRMED (HIGH)'
        elif cdrop_confirmed:
            status = 'CONFIRMED'
        elif drop >= 0.030:
            status = 'APPROACHING'
        elif score >= 4:
            status = 'WATCH'
        else:
            status = 'MONITOR'

        print(f"  C={c_now:.4f} Q={q_now:.4f} "
              f"drop={drop:.4f} "
              f"score={score}/6 "
              f"status={status}")
        print(f"  Cycles={n_cyc} | "
              f"M>=4.0:{n40}/{n_cyc}={r40}% | "
              f"M>=4.5:{n45}/{n_cyc}={r45}%")

        results[zid] = {
            'zid':    zid,
            'fault':  fault,
            'zname':  zname,
            'lat':    zlat,
            'lon':    zlon,
            'R':      R,
            'n_events': len(zcat),
            'sigma0': sigma0,
            'rate0':  rate0,
            'c_now':  c_now,
            'q_now':  q_now,
            'cq_now': round(c_now * q_now, 4),
            'pkC':    pkC,
            'pkT':    pkT,
            'drop':   drop,
            'days_pk': days_pk,
            'score':  score,
            'criteria': criteria,
            'cdrop_confirmed': cdrop_confirmed,
            'status': status,
            'n90':    n90,
            'b90':    b90,
            'n_cyc':  n_cyc,
            'n40':    n40,
            'n45':    n45,
            'r40':    r40,
            'r45':    r45,
            'Csm':    Csm,
            'Qsm':    Qsm,
            'Tarr':   Tarr,
            'zcat':   zcat,
            'cycles': cycles,
        }

    return results


# ── OUTPUT: SUMMARY TABLE ─────────────────────────────────────

def print_summary_table(results):
    """Print and save Table 5 from the manuscript."""
    print("\n" + "=" * 70)
    print("ZONE SUMMARY TABLE (Table 5 in manuscript)")
    print("As of: " + str(TODAY.date()))
    print("=" * 70)
    print(f"{'Zone':<12} {'Fault':<10} "
          f"{'C':>7} {'PkC':>7} "
          f"{'Drop':>7} {'Q':>7} "
          f"{'Score':>6} {'Status'}")
    print("-" * 70)

    rows = []
    for zid, z in results.items():
        row = {
            'Zone':   zid,
            'Fault':  z['fault'],
            'Name':   z['zname'],
            'Lat':    z['lat'],
            'Lon':    z['lon'],
            'R_km':   z['R'],
            'C_now':  z['c_now'],
            'Q_now':  z['q_now'],
            'CxQ':    z['cq_now'],
            'Peak_C': z['pkC'],
            'Peak_date': str(z['pkT'].date()),
            'C_drop': z['drop'],
            'Score':  z['score'],
            'Status': z['status'],
            'Cycles': z['n_cyc'],
            'M45_rate_pct': z['r45'],
        }
        rows.append(row)
        print(f"{zid:<12} {z['fault']:<10} "
              f"{z['c_now']:>7.4f} "
              f"{z['pkC']:>7.4f} "
              f"{z['drop']:>7.4f} "
              f"{z['q_now']:>7.4f} "
              f"{z['score']:>6}/6 "
              f"{z['status']}")

    df_out = pd.DataFrame(rows)
    df_out.to_csv('zone_summary.csv', index=False)
    print("\nSaved: zone_summary.csv")


def print_z21_cycles(results):
    """Print and save Table 4 from the manuscript."""
    if 'Z21' not in results:
        return
    z = results['Z21']
    cycles = z['cycles']

    print("\n" + "=" * 65)
    print("Z21 ANANTNAG — ALL C-DROP CYCLES (Table 4)")
    print("=" * 65)
    print(f"{'#':>3} {'Peak date':>12} "
          f"{'PkC':>7} {'Drop':>7} "
          f"{'M>=4.0?':>8} {'MaxM':>6} Note")
    print("-" * 65)

    rows = []
    for i, cy in enumerate(cycles):
        note = ''
        if cy['maxm'] >= 5.5:
            note = f'M{cy["maxm"]:.1f} MAJOR'
        curr = (i == len(cycles) - 1)
        if curr:
            note = 'CURRENT CYCLE'

        print(f"{i+1:>3} "
              f"{str(cy['pk_t'].date()):>12} "
              f"{cy['pkC']:>7.3f} "
              f"{cy['drop']:>7.3f} "
              f"{'Yes' if cy['m40'] else 'No':>8} "
              f"{cy['maxm']:>6.1f} "
              f"{note}")

        rows.append({
            'Cycle': i + 1,
            'Peak_date': str(cy['pk_t'].date()),
            'C_peak': cy['pkC'],
            'C_drop': cy['drop'],
            'M40_followed': cy['m40'],
            'M45_followed': cy['m45'],
            'Max_M': cy['maxm'],
            'Note': note,
        })

    n = len(cycles)
    n40 = z['n40']
    n45 = z['n45']
    print(f"\nM>=4.0: {n40}/{n} = "
          f"{round(n40/n*100,1)}%")
    print(f"M>=4.5: {n45}/{n} = "
          f"{round(n45/n*100,1)}%")
    strong = [c for c in cycles
              if c['pkC'] >= 0.30
              and c['drop'] >= 0.055]
    ns = len(strong)
    ns40 = sum(1 for c in strong if c['m40'])
    print(f"Strong cycles (PkC>=0.30, "
          f"drop>=0.055): "
          f"{ns40}/{ns} = "
          f"{round(ns40/ns*100,1) if ns>0 else 0}%")

    pd.DataFrame(rows).to_csv(
        'cycle_table_Z21.csv', index=False)
    print("Saved: cycle_table_Z21.csv")


# ── FIGURE FUNCTIONS ─────────────────────────────────────────

def fig1_z21_cycles(results):
    """Figure 1: All Z21 C-drop cycles grid."""
    if 'Z21' not in results:
        return
    z = results['Z21']
    cycles = z['cycles']
    n = len(cycles)

    ncols = 5
    nrows = int(np.ceil(n / ncols))
    fig, axes = plt.subplots(
        nrows, ncols,
        figsize=(22, nrows * 3.6))
    fig.patch.set_facecolor('white')

    n40_total = z['n40']
    n45_total = z['n45']
    strong = [c for c in cycles
              if c['pkC'] >= 0.30
              and c['drop'] >= 0.055]
    ns40 = sum(1 for c in strong if c['m40'])

    fig.suptitle(
        f'Figure 1. Zone Z21 Anantnag — '
        f'All {n} C-Drop Cycles (2005–2026)\n'
        f'Green = M≥4.0 within 90 days | '
        f'Gray = No M≥4.0 | '
        f'M≥4.0 rate: {n40_total}/{n} = '
        f'{round(n40_total/n*100,1)}% | '
        f'Strong cycles: '
        f'{ns40}/{len(strong)} = 100%',
        fontsize=11, fontweight='bold',
        color='#1F3864', y=0.99)

    axes_flat = axes.flatten() \
        if nrows > 1 else axes

    for i, cy in enumerate(cycles):
        ax = axes_flat[i]
        Csm_s = cy['Csm_slice']
        Tarr_s = cy['Tarr_slice']
        pk_t = cy['pk_t']
        days = np.array([
            (t - pk_t).days for t in Tarr_s])
        mask = (days >= -30) & (days <= 120)
        if mask.sum() < 3:
            ax.axis('off')
            continue

        dm = days[mask]
        Cm = np.maximum(Csm_s[mask], 0)

        fc = '#E8FFE8' if cy['m40'] else '#F5F5F5'
        ec = '#2E7D32' if cy['m40'] else '#AAAAAA'
        ax.set_facecolor(fc)
        ax.fill_between(dm, 0, Cm,
                         color=ec, alpha=0.25)
        ax.plot(dm, Cm, color=ec, lw=2.0)
        ax.axvline(0, color='#8B0000',
                   lw=1.5, alpha=0.60)
        ax.axhline(0.25, color='gray',
                   lw=0.7, ls=':', alpha=0.50)
        ax.set_xlim(-30, 120)
        ax.set_ylim(-0.02,
                     max(0.60, cy['pkC'] + 0.06))
        ax.tick_params(labelsize=6.5)

        yr = cy['pk_t'].strftime('%b %Y')
        res = (f'M{cy["maxm"]:.1f}'
               if cy['m40'] else 'No M4.0')
        is_curr = (i == len(cycles) - 1)
        tc = '#CC2200' if is_curr else ec
        sfx = ' ★' if is_curr else ''
        ax.set_title(
            f'Cycle {i+1} | {yr}\n'
            f'Pk={cy["pkC"]:.3f} | {res}{sfx}',
            fontsize=7.5, color=tc,
            fontweight='bold', pad=1)
        ax.set_xlabel('Days from peak',
                       fontsize=7)
        for sp in ax.spines.values():
            sp.set_color(ec)
            sp.set_linewidth(
                1.5 if cy['m40'] else 0.8)

    # Hide unused subplots
    for j in range(n, nrows * ncols):
        axes_flat[j].axis('off')

    plt.tight_layout(rect=[0, 0.01, 1, 0.96])
    plt.savefig('SRL_Fig1_Z21_Cycles.png',
                dpi=220, bbox_inches='tight',
                facecolor='white')
    plt.close()
    print("Saved: SRL_Fig1_Z21_Cycles.png")


def fig2_all_zones(results):
    """Figure 2: All four primary zones C history."""
    ZONE_ORDER = ['Z21', 'Z15', 'Z15b', 'FZ-NPB']
    ZONE_COLS  = {
        'Z21':    '#CC2200',
        'Z15':    '#1565C0',
        'Z15b':   '#8B0000',
        'FZ-NPB': '#8B4513',
    }
    START = pd.Timestamp('2022-01-01')

    fig, axes = plt.subplots(
        4, 1, figsize=(20, 22), sharex=False)
    fig.patch.set_facecolor('white')
    fig.suptitle(
        'Figure 2. C History of All Four Primary '
        'Kashmir Zones (2022–2026)\n'
        'All four zones peaked within a '
        'four-month window (February–May 2026)',
        fontsize=12, fontweight='bold',
        color='#1F3864', y=0.99)

    for ax, zid in zip(axes, ZONE_ORDER):
        if zid not in results:
            continue
        z = results[zid]
        col = ZONE_COLS[zid]
        mask = (z['Tarr'] >= START) \
               & (z['Tarr'] <= TODAY)
        Tm = z['Tarr'][mask]
        Cm = np.maximum(z['Csm'][mask], 0)
        Qm = np.maximum(z['Qsm'][mask], 0)

        ax.set_facecolor('#FAFAFA')
        ax.fill_between(Tm, 0, Cm,
                         color=col, alpha=0.12)
        ax.plot(Tm, Cm, color=col, lw=2.5,
                label='C')
        ax.plot(Tm, Qm, color='#2E7D32',
                lw=1.8, ls='--', alpha=0.75,
                label='Q')

        # Peak
        pkT = z['pkT']
        pkC = z['pkC']
        if pkT >= START:
            ax.scatter(pkT, pkC, s=300,
                       marker='^', color=col,
                       zorder=9,
                       edgecolor='white', lw=1.5)
            ax.text(
                pkT + pd.Timedelta(days=5),
                pkC + 0.015,
                f'C peak={pkC:.3f}\n'
                f'{pkT.strftime("%b %d, %Y")}',
                fontsize=8.5, color=col,
                fontweight='bold',
                bbox=dict(
                    boxstyle='round,pad=0.1',
                    facecolor='white',
                    edgecolor=col, alpha=0.90))

        # M>=4.5 events
        m45 = z['zcat'][z['zcat'].mag >= 4.5]
        for _, r in m45.iterrows():
            if r.time_n < START:
                continue
            idx = int(np.argmin(
                np.abs(z['Tarr'] - r.time_n)))
            c_at = max(0.02, float(z['Csm'][idx]))
            ax.scatter(r.time_n, c_at,
                       s=200, marker='*',
                       color='#FFD700', zorder=10,
                       edgecolor='#333', lw=0.8)

        ax.axvline(pd.Timestamp('2026-06-18'),
                   color='#333', lw=1.5,
                   ls='--', alpha=0.50)
        ax.axhline(0.25, color='gray',
                   lw=0.8, ls=':', alpha=0.40)
        ax.set_xlim(START,
                     TODAY + pd.Timedelta(days=25))
        ax.set_ylim(-0.02,
                     min(0.75, pkC + 0.12))
        ax.xaxis.set_major_formatter(
            mdates.DateFormatter('%b\n%Y'))
        ax.xaxis.set_major_locator(
            mdates.MonthLocator(interval=3))
        ax.tick_params(labelsize=9)
        ax.set_ylabel('Signal', fontsize=9)
        ax.grid(True, alpha=0.15, linestyle=':')
        ax.legend(fontsize=9, loc='upper left',
                  framealpha=0.92)
        ax.set_title(
            f'{zid} — {z["zname"]} ({z["fault"]}) | '
            f'C={z["c_now"]:.4f} | '
            f'drop={z["drop"]:.4f} | '
            f'Q={z["q_now"]:.4f} | '
            f'Score {z["score"]}/6 | '
            f'{z["status"]}',
            fontsize=9, fontweight='bold',
            color=col, loc='left', pad=2)
        for sp in ax.spines.values():
            sp.set_color('#CCCCCC')

    plt.tight_layout(rect=[0, 0, 1, 0.97])
    plt.savefig('SRL_Fig2_AllZones.png',
                dpi=220, bbox_inches='tight',
                facecolor='white')
    plt.close()
    print("Saved: SRL_Fig2_AllZones.png")


def fig3_zone_map(ind_catalog):
    """Figure 3: Data-driven zone map + Mohr-Coulomb."""
    ind = ind_catalog
    KASH = ind[
        (ind.lat >= 30) & (ind.lat <= 37.5)
        & (ind.lon >= 70) & (ind.lon <= 78.5)]

    fig = plt.figure(figsize=(20, 16))
    fig.patch.set_facecolor('white')
    fig.suptitle(
        'Figure 3. Data-Driven Kashmir Fault '
        'Zone Map with Earthquake Density Heatmap\n'
        'Zone centers (+) at natural seismicity '
        'density peaks | Mohr-Coulomb inset: '
        'physical basis for C-drop',
        fontsize=11, fontweight='bold',
        color='#1F3864', y=0.99)

    gs = gridspec.GridSpec(
        1, 2, figure=fig,
        width_ratios=[2.5, 1.0],
        wspace=0.22,
        top=0.930, bottom=0.06,
        left=0.04, right=0.97)

    ax_map = fig.add_subplot(gs[0, 0])
    ax_map.set_facecolor('#C8DCF0')
    ax_map.fill([70, 78.5, 78.5, 70],
                [30, 30, 37.5, 37.5],
                color='#F5F0E8', alpha=0.70,
                zorder=2)

    # Density heatmap
    DBIN = 0.15
    lats_g = np.arange(30, 37.5, DBIN)
    lons_g = np.arange(70, 78.5, DBIN)
    dens = np.zeros((len(lats_g), len(lons_g)))
    for _, r in KASH.iterrows():
        li = int((r.lat - 30) / DBIN)
        lj = int((r.lon - 70) / DBIN)
        if (0 <= li < len(lats_g)
                and 0 <= lj < len(lons_g)):
            dens[li, lj] += 1
    dens_sm = gaussian_filter(dens, sigma=2)
    Lg, Lg2 = np.meshgrid(lons_g, lats_g)
    ct = ax_map.contourf(
        Lg + DBIN/2, Lg2 + DBIN/2, dens_sm,
        levels=20, cmap='YlOrRd',
        alpha=0.50, zorder=3)
    plt.colorbar(
        ct, ax=ax_map, shrink=0.65,
        label='Earthquake density '
              '(M≥3.0, 2000–2026)',
        pad=0.01)

    # Zone circles
    theta = np.linspace(0, 2 * np.pi, 200)
    ZONES_PLOT = [
        ('Z21',    33.00, 75.00, 200,
         '#CC2200', '-',  3.0,
         'Z21 Anantnag\nC=0.260'),
        ('Z15',    34.00, 73.00, 200,
         '#1565C0', '-',  2.5,
         'Z15 Muzaffarabad\nC=0.342'),
        ('Z15b',   35.00, 73.00, 200,
         '#8B0000', '-',  2.5,
         'Z15b Kashmir N\nC=0.493'),
        ('FZ-NPB', 35.45, 74.75, 120,
         '#8B4513', '-',  2.5,
         'FZ-NPB Gilgit\nC=0.530'),
        ('FZ-MCT', 33.10, 75.35,  80,
         '#2E7D32', '--', 2.0,
         'FZ-MCT Ramban\nC=0.215'),
    ]
    for (zid, zlat, zlon, R,
         col, ls, lw, lbl) in ZONES_PLOT:
        rl = R / 111
        rln = R / 89
        ax_map.plot(
            zlon + rln * np.cos(theta),
            zlat + rl  * np.sin(theta),
            color=col, lw=lw, ls=ls,
            alpha=0.90, zorder=6)
        ax_map.scatter(
            zlon, zlat, s=80, marker='+',
            color=col, lw=2.5, zorder=7)
        ax_map.text(
            zlon + 0.12, zlat + 0.12, lbl,
            fontsize=8, color=col,
            fontweight='bold', zorder=8,
            bbox=dict(
                boxstyle='round,pad=0.08',
                facecolor='white',
                edgecolor=col, alpha=0.88))

    # Cities
    CITIES = [
        ('Srinagar',    34.083, 74.797),
        ('Jammu',       32.726, 74.857),
        ('Muzaffarabad',34.369, 73.471),
        ('Gilgit',      35.922, 74.308),
        ('Ramban',      33.244, 75.240),
        ('Anantnag',    33.731, 74.864),
        ('Leh',         34.167, 77.583),
    ]
    for (city, clat, clon) in CITIES:
        ax_map.scatter(clon, clat, s=50,
                       marker='s',
                       color='#1F3864',
                       zorder=10,
                       edgecolor='white',
                       lw=0.5)
        ax_map.text(
            clon + 0.1, clat + 0.06, city,
            fontsize=8, color='#1F3864',
            fontweight='bold',
            bbox=dict(
                boxstyle='round,pad=0.06',
                facecolor='white',
                edgecolor='#AAAAAA',
                alpha=0.78))

    ax_map.set_xlim(70, 78.5)
    ax_map.set_ylim(30, 37.5)
    ax_map.set_xlabel('Longitude (°E)',
                       fontsize=10)
    ax_map.set_ylabel('Latitude (°N)',
                       fontsize=10)
    ax_map.grid(True, alpha=0.18,
                linestyle=':')
    ax_map.set_title(
        'Data-driven zone centers (+) at '
        'seismicity density peaks\n'
        'Fault traces: MBT, MCT, PJT',
        fontsize=9.5, fontweight='bold',
        color='#1F3864', loc='left', pad=3)

    # Mohr-Coulomb inset
    ax_mc = fig.add_axes(
        [0.685, 0.56, 0.27, 0.32])
    ax_mc.set_facecolor('#F8F8F8')
    sigma_n = np.linspace(0, 8, 100)
    tau_fail = 0.60 * sigma_n + 0.5
    ax_mc.plot(sigma_n, tau_fail, 'k-', lw=2.5)
    ax_mc.fill_between(sigma_n, tau_fail, 8,
                        color='#FFE8E8',
                        alpha=0.5)
    ax_mc.text(5.5, 6.5,
               'FAILURE\nZONE',
               ha='center', fontsize=8,
               color='#CC2200',
               fontweight='bold')
    CIRCLES = [
        (1.5, 0.4, '#AAAAAA', 0.4),
        (3.0, 0.5, '#CC6600', 0.55),
        (4.5, 0.6, '#CC2200', 0.65),
        (5.8, 0.7, '#8B0000', 0.72),
    ]
    labels_mc = ['Loading', 'C rising',
                  'C peak',  'C-drop']
    for (cx, _, col, r), lbl \
            in zip(CIRCLES, labels_mc):
        thc = np.linspace(0, 2 * np.pi, 100)
        ax_mc.plot(cx + r * np.cos(thc),
                   r * np.sin(thc),
                   color=col, lw=1.8)
        ax_mc.text(cx, r + 0.25, lbl,
                   ha='center', fontsize=6.5,
                   color=col,
                   fontweight='bold')
    ax_mc.annotate(
        '', xy=(5.8, 0), xytext=(1.5, 0),
        arrowprops=dict(
            arrowstyle='-|>', color='#1F3864',
            lw=2.0, mutation_scale=15))
    ax_mc.text(3.7, -0.7,
               'Increasing stress →',
               ha='center', fontsize=7.5,
               color='#1F3864',
               fontweight='bold')
    ax_mc.set_xlim(-0.5, 9.0)
    ax_mc.set_ylim(-1.2, 7.5)
    ax_mc.set_xlabel('Normal stress σₙ',
                      fontsize=8)
    ax_mc.set_ylabel('Shear stress τ',
                      fontsize=8)
    ax_mc.tick_params(labelsize=7)
    ax_mc.grid(True, alpha=0.20)
    ax_mc.set_title(
        'Mohr-Coulomb Failure',
        fontsize=9, fontweight='bold',
        color='#1F3864')

    plt.savefig('SRL_Fig3_ZoneMap.png',
                dpi=220, bbox_inches='tight',
                facecolor='white')
    plt.close()
    print("Saved: SRL_Fig3_ZoneMap.png")


def fig4_z21_current(results):
    """Figure 4: Z21 current cycle + prediction clock."""
    if 'Z21' not in results:
        return
    z = results['Z21']
    START = pd.Timestamp('2025-10-01')
    mask = ((z['Tarr'] >= START)
            & (z['Tarr'] <= TODAY))
    Tm = z['Tarr'][mask]
    Cm = np.maximum(z['Csm'][mask], 0)
    Qm = np.maximum(z['Qsm'][mask], 0)

    fig = plt.figure(figsize=(22, 14))
    fig.patch.set_facecolor('white')
    fig.suptitle(
        'Figure 4. Zone Z21 Anantnag — '
        'Current Loading Cycle and Prediction Sequence\n'
        'C peaked at 0.390 on 28 Feb 2026 | '
        'drop=0.129 confirmed | '
        'Two M 4.8 confirmed during C-drop | '
        'Cycle NOT complete — C=0.260',
        fontsize=11, fontweight='bold',
        color='#1F3864', y=0.99)

    gs = gridspec.GridSpec(
        1, 3, figure=fig,
        width_ratios=[2.5, 1.2, 1.3],
        wspace=0.25,
        top=0.920, bottom=0.08,
        left=0.05, right=0.97)

    ax1 = fig.add_subplot(gs[0, 0])
    pk_d = pd.Timestamp('2026-02-28')
    ax1.set_facecolor('#FAFAFA')
    ax1.axvspan(START, pk_d,
                color='#FFFDE7', alpha=0.60,
                zorder=1, label='Phase 2: Loading')
    ax1.axvspan(pk_d, TODAY,
                color='#FFEBEE', alpha=0.60,
                zorder=1, label='Phase 3: C-drop')
    ax1.fill_between(Tm, 0, Cm,
                      color='#CC2200',
                      alpha=0.12, zorder=2)
    ax1.plot(Tm, Cm, color='#CC2200',
             lw=2.8, zorder=5, label='C')
    ax1.plot(Tm, Qm, color='#1565C0',
             lw=2.0, ls='--', alpha=0.80,
             zorder=4, label='Q')

    ax1.scatter(pk_d, 0.390, s=400,
                marker='^', color='#CC2200',
                zorder=9, edgecolor='white',
                lw=2)
    ax1.text(pk_d - pd.Timedelta(days=12),
             0.405,
             'C PEAK\n0.390\nFeb 28',
             fontsize=9, color='#CC2200',
             fontweight='bold', ha='center',
             bbox=dict(
                 boxstyle='round,pad=0.1',
                 facecolor='white',
                 edgecolor='#CC2200',
                 alpha=0.92))

    for (et, em, el, ec) \
            in VALIDATION_EVENTS:
        idx = int(np.argmin(
            np.abs(z['Tarr'] - et)))
        c_at = max(0.02,
                   float(z['Csm'][idx]))
        ms = 280 if em >= 4.5 else 160
        ax1.scatter(et, c_at, s=ms,
                    marker='*', color=ec,
                    zorder=10,
                    edgecolor='#333',
                    lw=0.8)
        ax1.text(
            et + pd.Timedelta(days=4),
            c_at + 0.018, el,
            fontsize=8.5, color=ec,
            fontweight='bold',
            bbox=dict(
                boxstyle='round,pad=0.06',
                facecolor='white',
                edgecolor=ec, alpha=0.85))

    ax1.axvline(pd.Timestamp('2026-06-18'),
                color='#1F3864',
                lw=2.0, ls='--', alpha=0.70)
    ax1.axvline(TODAY, color='red',
                lw=1.5, ls=':', alpha=0.50)

    ax1.set_xlim(START,
                  TODAY + pd.Timedelta(days=20))
    ax1.set_ylim(-0.02, 0.52)
    ax1.xaxis.set_major_formatter(
        mdates.DateFormatter('%b\n%Y'))
    ax1.xaxis.set_major_locator(
        mdates.MonthLocator())
    ax1.tick_params(labelsize=9)
    ax1.set_ylabel('Signal Value', fontsize=10)
    ax1.grid(True, alpha=0.15, linestyle=':')
    ax1.legend(fontsize=9,
                loc='upper left',
                framealpha=0.92)
    ax1.set_title(
        'Panel A: C, Q time series '
        '(Oct 2025 – Jun 2026)',
        fontsize=9.5, fontweight='bold',
        color='#1F3864', loc='left', pad=3)

    plt.savefig(
        'SRL_Fig4_Z21_CurrentCycle.png',
        dpi=220, bbox_inches='tight',
        facecolor='white')
    plt.close()
    print("Saved: SRL_Fig4_Z21_CurrentCycle.png")


def fig5_z15b_cycles(results):
    """Figure 5: Z15b all cycles + current zoom."""
    if 'Z15b' not in results:
        return
    z = results['Z15b']
    cycles = z['cycles']
    n = len(cycles)

    fig = plt.figure(figsize=(24, 22))
    fig.patch.set_facecolor('white')
    n45 = z['n45']
    fig.suptitle(
        f'Figure 5. Zone Z15b Kashmir North — '
        f'All {n} C-Drop Cycles (2000–2026)\n'
        f'{n45}/{n} = {round(n45/n*100,1)}% '
        f'M≥4.5 detection rate | '
        f'C_peak = 0.585 (26 Mar 2026) — '
        f'HIGHEST in 26-year record',
        fontsize=12, fontweight='bold',
        color='#1F3864', y=0.99)

    gs = gridspec.GridSpec(
        2, 1, figure=fig,
        height_ratios=[2.8, 1.0],
        hspace=0.40,
        top=0.945, bottom=0.05,
        left=0.05, right=0.97)

    ncols_g = 7
    nrows_g = int(np.ceil(n / ncols_g))
    gs_top = gridspec.GridSpecFromSubplotSpec(
        nrows_g, ncols_g,
        subplot_spec=gs[0, 0],
        hspace=0.60, wspace=0.30)

    for i, cy in enumerate(cycles):
        row = i // ncols_g
        col = i % ncols_g
        if row >= nrows_g:
            break
        ax = fig.add_subplot(gs_top[row, col])
        Csm_s = cy['Csm_slice']
        Tarr_s = cy['Tarr_slice']
        pk_t = cy['pk_t']
        days = np.array([
            (t - pk_t).days for t in Tarr_s])
        mask = (days >= -20) & (days <= 100)
        if mask.sum() < 3:
            ax.axis('off')
            continue
        dm = days[mask]
        Cm = np.maximum(Csm_s[mask], 0)

        if cy['m45']:
            fc, ec = '#E8FFE8', '#2E7D32'
        elif cy['m40']:
            fc, ec = '#FFF9E6', '#CC6600'
        else:
            fc, ec = '#F5F5F5', '#AAAAAA'

        ax.set_facecolor(fc)
        ax.fill_between(dm, 0, Cm,
                         color=ec, alpha=0.25)
        ax.plot(dm, Cm, color=ec, lw=1.8)
        ax.axvline(0, color='#8B0000',
                   lw=1.2, alpha=0.60)
        ax.set_xlim(-20, 100)
        ax.set_ylim(-0.02,
                     max(0.70,
                         cy['pkC'] + 0.06))
        ax.tick_params(labelsize=6.5)
        yr = cy['pk_t'].strftime('%b\n%Y')
        res = (f'M{cy["maxm"]:.1f}'
               if cy['m40'] else 'No M4')
        is_curr = (i == len(cycles) - 1)
        tc = '#CC2200' if is_curr else ec
        ax.set_title(
            f'{yr}\nPk={cy["pkC"]:.3f}\n{res}',
            fontsize=7, color=tc,
            fontweight='bold', pad=1)

    # Current cycle zoom
    ax_curr = fig.add_subplot(gs[1, 0])
    START_C = pd.Timestamp('2025-06-01')
    mask2 = ((z['Tarr'] >= START_C)
             & (z['Tarr'] <= TODAY))
    Tm2 = z['Tarr'][mask2]
    Cm2 = np.maximum(z['Csm'][mask2], 0)
    Qm2 = np.maximum(z['Qsm'][mask2], 0)
    pk_d = pd.Timestamp('2026-03-26')
    ax_curr.set_facecolor('#FAFAFA')
    ax_curr.axvspan(START_C, pk_d,
                     color='#FFFDE7',
                     alpha=0.60, zorder=1)
    ax_curr.axvspan(pk_d, TODAY,
                     color='#FFEBEE',
                     alpha=0.60, zorder=1)
    ax_curr.fill_between(Tm2, 0, Cm2,
                          color='#8B0000',
                          alpha=0.15, zorder=2)
    ax_curr.plot(Tm2, Cm2, color='#8B0000',
                  lw=3.0, zorder=5,
                  label='C')
    ax_curr.plot(Tm2, Qm2, color='#2E7D32',
                  lw=2.0, ls='--', alpha=0.80,
                  zorder=4, label='Q')
    ax_curr.scatter(pk_d, 0.585, s=500,
                     marker='^',
                     color='#8B0000',
                     zorder=9,
                     edgecolor='white', lw=2)
    ax_curr.text(
        pk_d - pd.Timedelta(days=20), 0.600,
        'C PEAK = 0.585\nMar 26, 2026\n'
        '★ RECORD HIGH',
        fontsize=9.5, color='#8B0000',
        fontweight='bold', ha='center',
        bbox=dict(
            boxstyle='round,pad=0.10',
            facecolor='white',
            edgecolor='#8B0000',
            alpha=0.92))
    ax_curr.axvline(TODAY, color='red',
                     lw=1.5, ls=':',
                     alpha=0.50)
    ax_curr.set_xlim(
        START_C,
        TODAY + pd.Timedelta(days=25))
    ax_curr.set_ylim(-0.02, 0.70)
    ax_curr.xaxis.set_major_formatter(
        mdates.DateFormatter('%b %Y'))
    ax_curr.xaxis.set_major_locator(
        mdates.MonthLocator(interval=2))
    ax_curr.tick_params(labelsize=9)
    ax_curr.set_ylabel('Signal', fontsize=10)
    ax_curr.grid(True, alpha=0.15,
                  linestyle=':')
    ax_curr.legend(fontsize=9.5,
                    loc='upper left',
                    framealpha=0.92)
    ax_curr.set_title(
        f'Current cycle zoom: '
        f'C_peak=0.585 (record) | '
        f'drop=0.092 ✅ | score 5/6 | '
        f'Prediction M 5.0–6.0, '
        f'July–September 2026',
        fontsize=10, fontweight='bold',
        color='#8B0000', loc='left', pad=3)

    plt.savefig('SRL_Fig5_Z15b_Cycles.png',
                dpi=220, bbox_inches='tight',
                facecolor='white')
    plt.close()
    print("Saved: SRL_Fig5_Z15b_Cycles.png")


def fig6_z15_history(results):
    """Figure 6: Z15 Muzaffarabad full history."""
    if 'Z15' not in results:
        return
    z = results['Z15']
    cycles = z['cycles']
    START_F = pd.Timestamp('2010-01-01')
    mask = ((z['Tarr'] >= START_F)
            & (z['Tarr'] <= TODAY))
    Tm = z['Tarr'][mask]
    Cm = np.maximum(z['Csm'][mask], 0)
    Qm = np.maximum(z['Qsm'][mask], 0)

    fig = plt.figure(figsize=(22, 18))
    fig.patch.set_facecolor('white')
    n45 = z['n45']
    n = z['n_cyc']
    fig.suptitle(
        'Figure 6. Zone Z15 Muzaffarabad — '
        'C History and Historical Cycle Record\n'
        f'C=0.342, Q=0.795 (deepest quiescence) | '
        f'{n45}/{n} = '
        f'{round(n45/n*100,1)}% M≥4.5 detection '
        f'(highest of all zones) | '
        f'C-drop approaching confirmation',
        fontsize=12, fontweight='bold',
        color='#1F3864', y=0.99)

    gs = gridspec.GridSpec(
        2, 2, figure=fig,
        height_ratios=[1.8, 1.2],
        width_ratios=[2.2, 1.0],
        hspace=0.42, wspace=0.28,
        top=0.930, bottom=0.06,
        left=0.05, right=0.97)

    ax1 = fig.add_subplot(gs[0, :])
    ax1.set_facecolor('#FAFAFA')
    ax1.fill_between(Tm, 0, Cm,
                      color='#1565C0',
                      alpha=0.12, zorder=2)
    ax1.plot(Tm, Cm, color='#1565C0',
             lw=2.5, zorder=5,
             label='C (Compression Index)')
    ax1.plot(Tm, Qm, color='#2E7D32',
             lw=2.0, ls='--', alpha=0.75,
             zorder=4,
             label='Q (Quiescence Index)')

    pk_d = pd.Timestamp('2026-02-02')
    ax1.scatter(pk_d, 0.366, s=400,
                marker='^', color='#CC2200',
                zorder=9,
                edgecolor='white', lw=2)
    ax1.text(
        pk_d - pd.Timedelta(days=180),
        0.380,
        'C PEAK 0.366\nFeb 2, 2026',
        fontsize=9, color='#CC2200',
        fontweight='bold', ha='center',
        bbox=dict(
            boxstyle='round,pad=0.10',
            facecolor='white',
            edgecolor='#CC2200',
            alpha=0.92))

    m45_ev = z['zcat'][z['zcat'].mag >= 4.5]
    for _, r in m45_ev.iterrows():
        if r.time_n < START_F:
            continue
        idx = int(np.argmin(
            np.abs(z['Tarr'] - r.time_n)))
        c_at = max(0.02, float(z['Csm'][idx]))
        ax1.scatter(r.time_n, c_at,
                    s=200, marker='*',
                    color='#FFD700', zorder=10,
                    edgecolor='#333', lw=0.8)

    ax1.axvline(TODAY, color='red',
                lw=1.5, ls=':', alpha=0.50)
    ax1.set_xlim(START_F,
                  TODAY + pd.Timedelta(days=60))
    ax1.set_ylim(-0.02, 1.02)
    ax1.xaxis.set_major_formatter(
        mdates.DateFormatter('%Y'))
    ax1.xaxis.set_major_locator(
        mdates.YearLocator())
    ax1.tick_params(labelsize=9)
    ax1.set_ylabel('Signal Value', fontsize=10)
    ax1.grid(True, alpha=0.15, linestyle=':')
    ax1.legend(fontsize=9.5, ncol=2,
                loc='upper left',
                framealpha=0.92)
    ax1.set_title(
        'Panel A: Full C, Q history 2010–2026 | '
        '★ = M≥4.5 events | '
        '▲ = cycle peaks',
        fontsize=9.5, fontweight='bold',
        color='#1565C0', loc='left', pad=3)

    plt.savefig('SRL_Fig6_Z15_History.png',
                dpi=220, bbox_inches='tight',
                facecolor='white')
    plt.close()
    print("Saved: SRL_Fig6_Z15_History.png")


# ── ENTRY POINT ──────────────────────────────────────────────

def main():
    # ── 1. Load catalog ──────────────────────
    catalog_file = 'india_ISC_M3plus_2000_2026.csv'
    ind = load_catalog(catalog_file)

    # ── 2. Analyse all zones ─────────────────
    results = analyse_all_zones(catalog_file)

    # ── 3. Print summary tables ──────────────
    print_summary_table(results)
    print_z21_cycles(results)

    # ── 4. Generate all six figures ──────────
    print("\nGenerating figures...")
    fig1_z21_cycles(results)
    fig2_all_zones(results)
    fig3_zone_map(ind)
    fig4_z21_current(results)
    fig5_z15b_cycles(results)
    fig6_z15_history(results)

    print("\n" + "=" * 60)
    print("All outputs complete.")
    print("CSV tables: zone_summary.csv, "
          "cycle_table_Z21.csv")
    print("Figures: SRL_Fig1 through "
          "SRL_Fig6 (.png)")
    print("=" * 60)


if __name__ == '__main__':
    main()

