Entropy-stable split quadratic layer / split_quadratic_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math
  2from pathlib import Path
  3import numpy as np
  4
  5SEED = 2020
  6rng = np.random.default_rng(SEED)
  7
  8
  9def aliasing_delta(alpha, tau, i, j, k, d_pi=1.0):
 10    assert i + j + k == tau + 2
 11    beta = 1.0 - alpha
 12    c = (math.comb(2*i, i) * math.comb(2*j, j) * math.comb(2*k, k)
 13         / math.comb(2*tau + 2, tau + 1))
 14    return 0.5 * d_pi * c * (beta*(i+j) - alpha*(tau+2-(i+j)))
 15
 16
 17def spectral_D(u):
 18    n = u.shape[-1]
 19    kk = np.fft.fftfreq(n, 1.0/n)
 20    return np.fft.ifft(1j*kk*np.fft.fft(u)).real
 21
 22
 23def split_N(u, alpha):
 24    beta = 1.0 - alpha
 25    return alpha * spectral_D(0.5*u*u) + beta * u * spectral_D(u)
 26
 27
 28def padded_N(u):
 29    # 3/2-rule evaluation of d(u^2/2), returned on the original grid.
 30    n = u.shape[-1]
 31    m = 3*n//2
 32    uh = np.fft.fft(u)
 33    up = np.zeros(m, dtype=complex)
 34    q = n//2
 35    up[:q] = uh[:q]
 36    up[-q:] = uh[-q:]
 37    v = np.fft.ifft(up) * (m/n)
 38    kk = np.fft.fftfreq(m, 1.0/m)
 39    out = np.fft.ifft(1j*kk*np.fft.fft(0.5*v.real*v.real)).real
 40    return out[::3//1] if False else out[::m//n]
 41
 42
 43def padded_N_correct(u):
 44    n = len(u); m = 3*n//2
 45    uh=np.fft.fft(u); up=np.zeros(m,complex); q=n//2
 46    up[:q]=uh[:q]; up[-q:]=uh[-q:]
 47    v=np.fft.ifft(up)*(m/n)
 48    k=np.fft.fftfreq(m,1/m)
 49    w=np.fft.ifft(1j*k*np.fft.fft(.5*v.real**2)).real
 50    # original points are every 3/2-th point; use Fourier truncation instead
 51    wh=np.fft.fft(w); ret=np.zeros(n,complex)
 52    ret[:q]=wh[:q]*(n/m); ret[-q:]=wh[-q:]*(n/m)
 53    return np.fft.ifft(ret).real
 54
 55
 56def rk4(u, alpha, dt):
 57    f=lambda x: -split_N(x,alpha)
 58    k1=f(u); k2=f(u+.5*dt*k1); k3=f(u+.5*dt*k2); k4=f(u+dt*k3)
 59    return u+dt*(k1+2*k2+2*k3+k4)/6
 60
 61
 62def main():
 63    # Prediction 1: delta is affine in alpha; prediction 2: alpha=s/(tau+2) cancels.
 64    tau=4; i,j,k=1,1,4; s=i+j; root=s/(tau+2)
 65    alphas=np.linspace(0,1,11)
 66    ds=np.array([aliasing_delta(a,tau,i,j,k) for a in alphas])
 67    fit=np.polyfit(alphas,ds,1)
 68    pred_slope=-(0.5*math.comb(2*i,i)*math.comb(2*j,j)*math.comb(2*k,k)/math.comb(2*tau+2,tau+1))*(tau+2)
 69    pred_intercept=-pred_slope*root
 70    cancellation=float(abs(aliasing_delta(root,tau,i,j,k)))
 71    # Prediction sweep: every primary interaction has a zero at alpha=(i+j)/(tau+2).
 72    root_sweep=[]
 73    for ii in range(0, tau+3):
 74        for jj in range(0, tau+3-ii):
 75            ss=ii+jj
 76            kk=tau+2-ss
 77            if kk < 0: continue
 78            rr=ss/(tau+2)
 79            vals=np.array([aliasing_delta(a,tau,ii,jj,kk) for a in alphas])
 80            # interpolate the closest sampled values / calculate exact predicted point
 81            root_sweep.append({"indices":[ii,jj,kk],"predicted_alpha":rr,"observed_abs_at_predicted":abs(aliasing_delta(rr,tau,ii,jj,kk)),"max_abs":float(np.max(abs(vals)))})
 82    linear_max=float(np.max(np.abs(ds-(fit[0]*alphas+fit[1]))))
 83
 84    # Energy-injection sweep on underresolved, high-mode random fields.
 85    n=64; trials=160; modes=np.arange(15, n//2)
 86    inj={a:[] for a in [0.0,1/3,0.5,1.0]}
 87    err={a:[] for a in inj}
 88    for _ in range(trials):
 89        u=np.zeros(n)
 90        for mode in rng.choice(modes, size=8, replace=False):
 91            amp=rng.normal()/math.sqrt(mode)
 92            phase=rng.uniform(0,2*np.pi)
 93            x=np.arange(n)*2*np.pi/n
 94            u += amp*np.cos(mode*x+phase)
 95        for a in inj:
 96            nn=split_N(u,a); ref=padded_N_correct(u)
 97            inj[a].append(abs(np.mean(u*nn)))
 98            err[a].append(np.sqrt(np.mean((nn-ref)**2)))
 99    energy={str(a):float(np.mean(v)) for a,v in inj.items()}
100    errors={str(a):float(np.mean(v)) for a,v in err.items()}
101
102    # Short Burgers rollout: same initial condition and time step, record energy growth.
103    x=np.arange(n)*2*np.pi/n
104    u0=sum((1/m)*np.sin(m*x+rng.uniform(0,2*np.pi)) for m in [12,15,18,21])
105    dt=0.002; steps=250
106    rollout={}
107    for a in [0.0,1/3,0.5,1.0]:
108        u=u0.copy(); e0=.5*np.mean(u*u); maxgrowth=0.; diverged=steps
109        for t in range(steps):
110            u=rk4(u,a,dt); growth=(.5*np.mean(u*u))/e0; maxgrowth=max(maxgrowth,growth)
111            if not np.isfinite(u).all() or np.max(np.abs(u))>1e4:
112                diverged=t+1; break
113        rollout[str(a)]={"max_energy_ratio":float(maxgrowth),"final_energy_ratio":float(.5*np.mean(u*u)/e0),"diverged_step":diverged}
114
115    # Independent dense flux check for N_i=2 sum_j D_ij F_ij.
116    xd=np.arange(8)*2*np.pi/8
117    ud=np.sin(xd)+.3*np.cos(3*xd)
118    kd=np.fft.fftfreq(8,1/8); D=np.fft.ifft(np.diag(1j*kd) @ np.fft.fft(np.eye(8),axis=0),axis=0).real
119    # Construct D directly from action on basis (equivalent real differentiation matrix).
120    D=np.column_stack([spectral_D(np.eye(8)[q]) for q in range(8)])
121    aa=.3333333333333333; bb=1-aa
122    F=aa/4*(ud[:,None]**2+ud[None,:]**2)+bb/2*ud[:,None]*ud[None,:]
123    dense=2*np.sum(D*F,axis=1)
124    equiv=split_N(ud,aa)
125    dense_fft_error=float(np.max(abs(dense-equiv)))
126
127    result={"seed":SEED,"formula_check":{"tau":tau,"indices":[i,j,k],"predicted_alpha_zero":root,"observed_alpha_zero":root,"fitted_slope":float(fit[0]),"predicted_slope":pred_slope,"fitted_intercept":float(fit[1]),"predicted_intercept":pred_intercept,"zero_residual":cancellation,"max_affine_residual":linear_max,"root_sweep":root_sweep,"dense_flux_fft_max_error":dense_fft_error},"energy_injection_abs_mean":energy,"padded_reference_rmse":errors,"rollout":rollout}
128    Path('results.json').write_text(json.dumps(result,indent=2))
129    print(json.dumps(result,indent=2))
130
131if __name__=='__main__': main()