Fourier-Mode Stability Shaping / fourier_stability.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
  1import json
  2from pathlib import Path
  3import numpy as np
  4from scipy.optimize import minimize
  5
  6
  7def mu_factors(N, m, q, weights=None):
  8    weights = np.ones(m) if weights is None else np.asarray(weights)
  9    ell = np.arange(1, m + 1)
 10    cq = np.cos(2 * np.pi * q * ell / N)
 11    return np.array([np.sum(weights * cq * (1 - np.cos(2 * np.pi * k * ell / N))) for k in range(N)])
 12
 13
 14def eigenvalues(N, m, q, kappa=1.0, weights=None):
 15    weights = np.ones(m) if weights is None else np.asarray(weights)
 16    ell = np.arange(1, m + 1)
 17    cq = np.cos(2 * np.pi * q * ell / N)
 18    return np.array([kappa * np.sum(weights * cq * (np.exp(2j * np.pi * k * ell / N) - 1)) for k in range(N)])
 19
 20
 21def rhs(theta, m, kappa=1.0, weights=None):
 22    weights = np.ones(m) if weights is None else np.asarray(weights)
 23    y = np.zeros_like(theta)
 24    for l, a in enumerate(weights, 1):
 25        y += a * np.sin(np.roll(theta, -l) - theta)
 26    return kappa * y
 27
 28
 29def mode_amplitude(x, profile, k):
 30    N = len(x)
 31    u = np.exp(2j * np.pi * k * np.arange(N) / N)
 32    return abs(np.vdot(u, x - profile) / N)
 33
 34
 35def simulate(profile, m, k, kappa, h, steps, weights=None, eps=1e-7):
 36    N = len(profile)
 37    u = np.exp(2j * np.pi * k * np.arange(N) / N)
 38    x = profile + eps * np.real(u)
 39    amps = [mode_amplitude(x, profile, k)]
 40    for _ in range(steps):
 41        x = x + h * rhs(x, m, kappa, weights)
 42        amps.append(mode_amplitude(x, profile, k))
 43    return np.asarray(amps)
 44
 45
 46def fit_slope(a, h):
 47    t = np.arange(len(a)) * h
 48    return float(np.polyfit(t, np.log(np.maximum(a, 1e-30)), 1)[0])
 49
 50
 51def main():
 52    N, m, q, k = 32, 4, 2, 1
 53    profile = 2 * np.pi * q * np.arange(N) / N
 54    mu = mu_factors(N, m, q)
 55    lam = eigenvalues(N, m, q)[k]
 56    # Prediction 1: infinitesimal slope equals -mu.
 57    a = simulate(profile, m, k, 1.0, .002, 1000)
 58    slope = fit_slope(a, .002)
 59    # Prediction 2: slope scales linearly with coupling.
 60    scaling = []
 61    for kap in [.25, .5, 1., 1.5]:
 62        aa = simulate(profile, m, k, kap, .001, 500)
 63        scaling.append({'kappa': kap, 'predicted': float(-kap * mu[k]), 'observed': fit_slope(aa, .001)})
 64    # Prediction 3: explicit-Euler boundary |1+h lambda|=1.
 65    lb = eigenvalues(N, m, 0)[5]
 66    hcrit = float(-2 * lb.real / abs(lb) ** 2)
 67    hs = np.linspace(.2 * hcrit, 1.8 * hcrit, 65)
 68    stable = np.abs(1 + hs * lb) <= 1
 69    crossings = np.where(stable[:-1] & ~stable[1:])[0]
 70    observed_hcrit = float((hs[crossings[0]] + hs[crossings[0] + 1]) / 2)
 71    # Negative-factor prediction.
 72    candidates = [(qq, kk, float(mu_factors(N, m, qq)[kk])) for qq in range(N) for kk in range(1, N)
 73                  if mu_factors(N, m, qq)[kk] < -.05]
 74    qq, kk, negmu = candidates[0]
 75    prof_neg = 2 * np.pi * qq * np.arange(N) / N
 76    an = simulate(prof_neg, m, kk, 1., .001, 1000)
 77    neg_slope = fit_slope(an, .001)
 78    # Baseline versus Fourier-shaped coupling on the same unstable profile/mode.
 79    baseline_w = np.ones(m)
 80    target = .25
 81    objective = lambda w: (mu_factors(N, m, qq, w)[kk] - target) ** 2 + .02 * np.sum((w - 1.) ** 2)
 82    opt = minimize(objective, baseline_w, method='L-BFGS-B', bounds=[(-3., 3.)] * m)
 83    shaped_w = opt.x
 84    base_mu = float(mu_factors(N, m, qq, baseline_w)[kk])
 85    shaped_mu = float(mu_factors(N, m, qq, shaped_w)[kk])
 86    base_amp = simulate(prof_neg, m, kk, 1., .001, 1000, baseline_w)
 87    shaped_amp = simulate(prof_neg, m, kk, 1., .001, 1000, shaped_w)
 88    comparison = {'profile_q': qq, 'mode': kk, 'target_mu': target,
 89      'baseline': {'weights': baseline_w.tolist(), 'mu': base_mu, 'slope': fit_slope(base_amp, .001),
 90                   'amplitude_ratio': float(base_amp[-1] / base_amp[0])},
 91      'fourier_shaped': {'weights': shaped_w.tolist(), 'mu': shaped_mu, 'slope': fit_slope(shaped_amp, .001),
 92                         'amplitude_ratio': float(shaped_amp[-1] / shaped_amp[0])}}
 93    report = {'config': {'N': N, 'm': m, 'q': q, 'mode': k},
 94      'growth_rate_test': {'predicted': float(-mu[k]), 'observed': slope, 'relative_error': abs(slope + mu[k]) / abs(mu[k])},
 95      'coupling_scaling_test': scaling,
 96      'euler_boundary_test': {'lambda': [float(lb.real), float(lb.imag)], 'predicted_hcrit': hcrit,
 97          'observed_hcrit': observed_hcrit, 'relative_error': abs(observed_hcrit - hcrit) / hcrit},
 98      'negative_factor_test': {'q': qq, 'mode': kk, 'mu': negmu, 'predicted_growth': -negmu,
 99          'observed_growth': neg_slope, 'relative_error': abs(neg_slope + negmu) / abs(negmu)},
100      'baseline_vs_shaped': comparison}
101    Path('results.json').write_text(json.dumps(report, indent=2))
102    print(json.dumps(report, indent=2))
103
104if __name__ == '__main__':
105    main()