"""
Z9 nine-mode Galerkin shell model — PTRH Paper 29.

Implements the nine-mode system with Z9 cyclic triadic coupling
(GOY-type energy-conserving structure, cyclic boundary conditions).
Demonstrates:
  1. Bounded attractor — no finite-time blowup in the truncation.
  2. Time-averaged energy spectrum compared to Gaussian weights and k^{-5/3}.
  3. Cascade time structure across nine sectors.

Outputs (written to the script directory):
  p29_spectrum.pdf  — publication figure for inclusion in the paper
  p29_spectrum.pgf  — PGF version for LaTeX embedding
"""

import numpy as np
from scipy.integrate import solve_ivp
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import pathlib

# ── Parameters ─────────────────────────────────────────────────────────────
N       = 9           # number of sectors
k0      = 1.0         # integral-scale wavenumber
lam     = 9.0**(1.0/N)  # inter-shell ratio 9^{1/9}
nu      = 1e-2        # kinematic viscosity
f_amp   = 1e-2        # forcing amplitude at n=0
T_total = 3000.0      # total integration time
T_avg   = 1000.0      # begin time-averaging after transient
seed    = 2026

OUTDIR  = pathlib.Path(__file__).parent

# ── Shell wavenumbers ───────────────────────────────────────────────────────
kn = k0 * 9.0**(np.arange(N) / float(N))   # k_n = k0 * 9^{n/9}

# ── Z9 GOY-type shell model ─────────────────────────────────────────────────
# Coupling:
#   du_n/dt = i [ k_n u*_{n+1} u_{n+2}
#               - (k_{n-1}/2) u*_{n-1} u_{n+1}
#               - (k_{n-2}/2) u_{n-1} u*_{n-2} ]
#             - nu k_n^2 u_n + f delta_{n,0}
# All shell indices taken mod N.
# Energy conservation in inviscid unforced limit follows from the standard
# GOY cancellation argument applied cyclically (Gledzer 1973;
# Ohkitani & Yamada 1989).

def rhs(t, y):
    u = y[:N] + 1j * y[N:]
    du = np.zeros(N, dtype=complex)
    for n in range(N):
        p  = (n + 1) % N
        q  = (n + 2) % N
        r  = (n - 1) % N
        s  = (n - 2) % N
        nl = 1j * (  kn[n]   * np.conj(u[p]) * u[q]
                   - kn[r]/2 * np.conj(u[r]) * u[p]
                   - kn[s]/2 * u[r]           * np.conj(u[s]) )
        du[n] = nl - nu * kn[n]**2 * u[n]
    du[0] += f_amp                          # large-scale forcing at sector 0
    return np.concatenate([du.real, du.imag])

# ── Initial conditions ──────────────────────────────────────────────────────
rng  = np.random.default_rng(seed)
u0_c = 1e-2 * (rng.standard_normal(N) + 1j * rng.standard_normal(N))
y0   = np.concatenate([u0_c.real, u0_c.imag])

# ── Integrate ───────────────────────────────────────────────────────────────
print(f"Integrating Z9 shell model (T={T_total}, nu={nu}, f={f_amp}) ...")
sol = solve_ivp(
    rhs, [0.0, T_total], y0,
    method='Radau', rtol=1e-8, atol=1e-10,
    dense_output=False,
    max_step=0.5,
)
print(f"  Steps: {len(sol.t):,}  |  Success: {sol.success}  |  t_end: {sol.t[-1]:.1f}")
if not sol.success:
    print(f"  WARNING: {sol.message}")

# ── Extract solution ────────────────────────────────────────────────────────
t_arr  = sol.t
u_arr  = sol.y[:N] + 1j * sol.y[N:]        # shape (N, n_steps)
E_arr  = 0.5 * np.abs(u_arr)**2            # instantaneous sector energies

E_tot  = E_arr.sum(axis=0)                 # total energy time series

# Time-averaged spectrum (post-transient)
mask      = t_arr >= T_avg
E_avg     = E_arr[:, mask].mean(axis=1)    # shape (N,)
E_avg_norm = E_avg / E_avg.sum()

# ── Reference distributions ─────────────────────────────────────────────────
# Gaussian weights w_n = C exp(-pi n^2 / 9)
n_arr = np.arange(N, dtype=float)
w_raw = np.exp(-np.pi * n_arr**2 / 9.0)
w_n   = w_raw / w_raw.sum()

# Kolmogorov k^{-5/3}, anchored to E_avg[0]
k_fine  = np.linspace(kn[0]*0.9, kn[-1]*1.3, 300)
kolm    = E_avg[0] * (k_fine / kn[0])**(-5.0/3.0)

# ── Figures ─────────────────────────────────────────────────────────────────
plt.rcParams.update({
    'font.family'      : 'serif',
    'font.size'        : 9,
    'axes.labelsize'   : 9,
    'legend.fontsize'  : 8,
    'xtick.labelsize'  : 8,
    'ytick.labelsize'  : 8,
    'figure.dpi'       : 150,
    'text.usetex'      : False,
})

fig = plt.figure(figsize=(6.8, 4.5))
gs  = gridspec.GridSpec(1, 2, figure=fig, wspace=0.38)

# ── Panel (a): total energy time series ─────────────────────────────────────
ax1 = fig.add_subplot(gs[0])
ax1.semilogy(t_arr, E_tot, lw=0.6, color='steelblue', alpha=0.8)
ax1.axvline(T_avg, ls='--', lw=0.9, color='grey', label=f'$t={T_avg:.0f}$ (avg start)')
ax1.set_xlabel('Time $t$')
ax1.set_ylabel('Total energy $\\mathcal{E}(t)$')
ax1.set_title('(a) Bounded attractor: total energy')
ax1.legend(fontsize=7)
ax1.set_xlim(0, T_total)

# ── Panel (b): time-averaged energy spectrum ─────────────────────────────────
ax2 = fig.add_subplot(gs[1])

# Z9 model time-averaged spectrum
ax2.loglog(kn, E_avg, 'o-', ms=5, lw=1.4, color='steelblue',
           label='$Z_9$ model $\\langle E_n\\rangle_T$')

# Gaussian weights (scaled to same total)
ax2.loglog(kn, w_n * E_avg.sum(), 's--', ms=4, lw=1.0, color='firebrick',
           label='TMRB Gaussian $w_n$ (scaled)')

# Kolmogorov k^{-5/3}
ax2.loglog(k_fine, kolm, ':', lw=1.5, color='darkorange',
           label='$k^{-5/3}$ reference')

# Spectral gap markers
delta_min = 2 * np.sin(np.pi / 9.0)
for i in range(N - 1):
    ax2.annotate('', xy=(kn[i+1], E_avg[i+1]),
                 xytext=(kn[i], E_avg[i]),
                 arrowprops=dict(arrowstyle='->', color='grey', lw=0.7))

ax2.set_xlabel('Wavenumber $k_n$')
ax2.set_ylabel('Energy $E_n$')
ax2.set_title('(b) Time-averaged energy spectrum')
ax2.legend(loc='upper right')
ax2.text(0.03, 0.07,
         f'$\\Delta_{{\\rm min}} = 2\\sin(\\pi/9) \\approx {delta_min:.3f}$',
         transform=ax2.transAxes, fontsize=7.5, color='darkgreen')

fig.suptitle(
    f'Nine-mode $Z_9$ Galerkin model  '
    f'($\\nu={nu}$, $f={f_amp}$, $T_{{\\rm avg}}={T_avg:.0f}$)',
    fontsize=9, y=1.01
)

fig.tight_layout()
outpath = OUTDIR / 'p29_spectrum.pdf'
fig.savefig(str(outpath), bbox_inches='tight')
print(f"Figure saved: {outpath}")

# ── Print numerical summary ──────────────────────────────────────────────────
print("\n-- Time-averaged sector energies (post-transient) --")
print(f"{'n':>3}  {'k_n':>8}  {'E_avg':>12}  {'E_norm':>10}  {'w_n':>10}")
for i in range(N):
    print(f"{i:3d}  {kn[i]:8.4f}  {E_avg[i]:12.6e}  "
          f"{E_avg_norm[i]:10.6f}  {w_n[i]:10.6f}")

print(f"\nTotal energy post-transient: {E_avg.sum():.6e}")
print(f"Energy in sector 0: {100*E_avg_norm[0]:.1f}%  "
      f"(Gaussian predicts {100*w_n[0]:.1f}%)")
print(f"\nMin/max total energy (full run): "
      f"{E_tot.min():.4e} / {E_tot.max():.4e}")
print(f"Attractor bounded: max/min energy ratio = {E_tot.max()/E_tot.min():.2f}")

np.savez(OUTDIR / 'p29_spectrum_data.npz',
         t=t_arr, E_tot=E_tot, kn=kn, E_avg=E_avg,
         E_avg_norm=E_avg_norm, w_n=w_n)
print("\nData saved: p29_spectrum_data.npz")
