Adjoint-Weak Fractional Residuals / experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json
  2import math
  3import numpy as np
  4from pathlib import Path
  5
  6# Adjoint-weak fractional residual MVP.
  7# The Grünwald matrix is used only to make the discrete integration-by-parts
  8# identity explicit; weak measurements use its exact transpose.
  9
 10def grunwald_matrix(n, alpha, T=1.0):
 11    h = T / (n - 1)
 12    # left-sided GL derivative: (D f)_i = h^-alpha sum_{k<=i} (-1)^k C(alpha,k) f_{i-k}
 13    w = np.empty(n)
 14    w[0] = 1.0
 15    for k in range(1, n):
 16        w[k] = w[k-1] * (-(alpha-k+1)/k)
 17    A = np.zeros((n, n))
 18    for i in range(n):
 19        A[i, :i+1] = h**(-alpha) * w[:i+1][::-1]
 20    return A
 21
 22def trapezoid_weights(n, T=1.0):
 23    h = T/(n-1)
 24    q = np.full(n, h)
 25    q[[0,-1]] = h/2
 26    return q
 27
 28def gaussian_phi(t, center=.62, width=.16):
 29    return np.exp(-0.5*((t-center)/width)**2)
 30
 31def one_check(n, alpha, sigma, rng):
 32    hq = trapezoid_weights(n)
 33    A = grunwald_matrix(n, alpha)
 34    phi = gaussian_phi(np.linspace(0,1,n))
 35    # Integral phi^T W D f = f^T D^T W phi.
 36    lhs = phi @ (hq * (A @ rng.normal(size=n)))
 37    qweak = A.T @ (hq * phi)
 38    rhs = qweak @ rng.normal(size=n) # independent draw, used only for norm check below
 39    # exact coefficient vector acting on sampled f
 40    q = qweak
 41    return float(np.dot(q,q)), float(np.dot(hq*phi, hq*phi)), A, q, phi, hq
 42
 43def noisy_variance_sweep():
 44    rng = np.random.default_rng(11)
 45    n, alpha = 257, .8
 46    A = grunwald_matrix(n, alpha)
 47    t = np.linspace(0,1,n); hq = trapezoid_weights(n)
 48    phi = gaussian_phi(t)
 49    qweak = A.T @ (hq*phi)
 50    # A pointwise derivative at an interior location, versus one weak scalar.
 51    i = n//2
 52    qstrong = A[i].copy()
 53    rows=[]
 54    for sigma in [.01,.03,.1,.3]:
 55        vals_w=[]; vals_s=[]
 56        for _ in range(5000):
 57            e=rng.normal(0,sigma,n)
 58            vals_w.append(qweak@e); vals_s.append(qstrong@e)
 59        vw=np.var(vals_w, ddof=1); vs=np.var(vals_s, ddof=1)
 60        rows.append({'sigma':sigma,'weak_var':vw,'strong_var':vs,
 61                     'weak_over_sigma2':vw/sigma**2,'strong_over_sigma2':vs/sigma**2})
 62    return rows
 63
 64def resolution_sweep():
 65    alpha=.8; rng=np.random.default_rng(12); out=[]
 66    for n in [65,129,257,513]:
 67        t=np.linspace(0,1,n); hq=trapezoid_weights(n)
 68        A=grunwald_matrix(n,alpha); phi=gaussian_phi(t)
 69        qw=A.T@(hq*phi); qs=A[n//2]
 70        out.append({'n':n,'weak_norm2':float(qw@qw),'strong_norm2':float(qs@qs)})
 71    # consecutive ratios are the directly testable scaling predictions
 72    for i in range(1,len(out)):
 73        out[i]['weak_ratio_prev']=out[i]['weak_norm2']/out[i-1]['weak_norm2']
 74        out[i]['strong_ratio_prev']=out[i]['strong_norm2']/out[i-1]['strong_norm2']
 75    return out
 76
 77def coefficient_fit(seed, sigma, n=129, alpha=.8, trials=200):
 78    rng=np.random.default_rng(seed); t=np.linspace(0,1,n); hq=trapezoid_weights(n)
 79    A=grunwald_matrix(n,alpha); phi=gaussian_phi(t)
 80    # Synthetic relation D^alpha u = c u on a fixed smooth trajectory. We estimate
 81    # c from noisy sampled u; weak form transfers D to phi.
 82    u=np.exp(-.8*t)*(1+.2*np.sin(2*np.pi*t)); ctrue=.7
 83    # use a generated target equal to the discrete operator, avoiding continuum mismatch
 84    target=A@u
 85    qweak=A.T@(hq*phi); qid=hq*phi
 86    # Compare noise-induced error to each estimator's noiseless value; this
 87    # isolates robustness without claiming the arbitrary trajectory obeys a
 88    # pointwise constant-coefficient fractional ODE.
 89    weak0=(qweak@u)/(qid@u)
 90    inds=np.arange(n//2-3,n//2+4)
 91    strong0=np.sum(u[inds]*(A[inds]@u))/np.sum(u[inds]**2)
 92    csw=[]; css=[]
 93    for _ in range(trials):
 94        un=u+rng.normal(0,sigma,n)
 95        # weak scalar equation: int phi D u = c int phi u
 96        den=qid@un; csw.append((qweak@un)/den)
 97        # strong local equation, average a central neighborhood for fair scalar noise
 98        inds=np.arange(n//2-3,n//2+4)
 99        d=A[inds]@un
100        css.append(np.sum(un[inds]*d)/np.sum(un[inds]**2))
101    return {'sigma':sigma,'weak_rmse':float(np.sqrt(np.mean((np.array(csw)-weak0)**2))),
102            'strong_rmse':float(np.sqrt(np.mean((np.array(css)-strong0)**2))),
103            'weak_noiseless':float(weak0),'strong_noiseless':float(strong0)}
104
105def identity_check():
106    rng=np.random.default_rng(4); n=97; alpha=.65
107    A=grunwald_matrix(n,alpha); q=trapezoid_weights(n); f=rng.normal(size=n); phi=rng.normal(size=n)
108    lhs=phi@(q*(A@f)); rhs=f@(A.T@(q*phi))
109    return {'absolute_error':float(abs(lhs-rhs)), 'relative_error':float(abs(lhs-rhs)/(1+abs(lhs)))}
110
111def main():
112    result={'identity':identity_check(), 'noise_sweep':noisy_variance_sweep(),
113            'resolution_sweep':resolution_sweep(),
114            'coefficient_fit':[coefficient_fit(100+i,s) for i,s in enumerate([.01,.03,.1,.3])]}
115    # Predictions from the mechanism: variance is sigma^2 ||q||^2; weak q norm
116    # scales approximately h^(1/2) for a fixed smooth localized kernel. The
117    # GL point-row norm is dominated by summable near-diagonal weights, so its
118    # squared norm scales as h^(-2 alpha), giving ratio 2^(2 alpha).
119    r=result['resolution_sweep']
120    result['predictions']={
121      'noise_linearity':'weak variance / sigma^2 should be constant',
122      'weak_resolution_ratio_predicted':0.5,
123      'strong_resolution_ratio_predicted':2**(2*.8),
124      'observed_weak_ratios':[x.get('weak_ratio_prev') for x in r[1:]],
125      'observed_strong_ratios':[x.get('strong_ratio_prev') for x in r[1:]],
126      'identity_tolerance':'machine precision'}
127    Path('results.json').write_text(json.dumps(result,indent=2))
128    print(json.dumps(result,indent=2))
129
130if __name__=='__main__': main()