Wasserstein-Controlled Gaussian-Mixture Rollouts / gm_rollout_experiment.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math
  2import numpy as np
  3from scipy.special import ndtr
  4
  5SEED = 1729
  6rng = np.random.default_rng(SEED)
  7
  8def f(x):
  9    return 0.85*x + 0.20*np.tanh(x)
 10
 11def jac(x):
 12    return 0.85 + 0.20/(np.cosh(x)**2)
 13
 14class Comp:
 15    __slots__ = ('w','m','v')
 16    def __init__(self, w, m, v): self.w, self.m, self.v = float(w), float(m), float(v)
 17
 18def normalize(cs):
 19    s = sum(c.w for c in cs)
 20    for c in cs: c.w /= s
 21    return cs
 22
 23def gaussian_w2(a, b):
 24    return math.sqrt((a.m-b.m)**2 + (math.sqrt(max(a.v,0))-math.sqrt(max(b.v,0)))**2)
 25
 26def merge(cs, eps):
 27    cs = list(cs)
 28    while len(cs) > 1:
 29        cand = [(gaussian_w2(cs[i],cs[j]),i,j) for i in range(len(cs)) for j in range(i+1,len(cs))]
 30        d,i,j = min(cand)
 31        if d > eps: break
 32        a,b = cs[i],cs[j]; w = a.w+b.w; m = (a.w*a.m+b.w*b.m)/w
 33        v = (a.w*(a.v+(a.m-m)**2)+b.w*(b.v+(b.m-m)**2))/w
 34        cs = [c for k,c in enumerate(cs) if k not in (i,j)] + [Comp(w,m,v)]
 35    return normalize(cs)
 36
 37def propagate(cs, d, q, eps=None):
 38    out=[]
 39    for c in cs:
 40        J=jac(c.m)
 41        out += [Comp(.5*c.w, f(c.m)-d, J*J*c.v+q), Comp(.5*c.w, f(c.m)+d, J*J*c.v+q)]
 42    return merge(out,eps) if eps is not None else normalize(out)
 43
 44def moments(cs):
 45    m=sum(c.w*c.m for c in cs)
 46    v=sum(c.w*(c.v+(c.m-m)**2) for c in cs)
 47    return m,v
 48
 49def chance(cs, threshold):
 50    return sum(c.w*ndtr((threshold-c.m)/math.sqrt(c.v)) for c in cs)
 51
 52def sample(cs, n, rg):
 53    ix=rg.choice(len(cs), n, p=[c.w for c in cs])
 54    return np.array([rg.normal(cs[k].m, math.sqrt(cs[k].v)) for k in ix])
 55
 56def empirical_w2(x,y):
 57    # In one dimension the sorted empirical coupling is optimal W2.
 58    n=min(len(x),len(y)); xx=np.sort(x[:n]); yy=np.sort(y[:n])
 59    return math.sqrt(np.mean((xx-yy)**2))
 60
 61def gaussian_component(cs):
 62    m,v=moments(cs)
 63    return [Comp(1,m,v)]
 64
 65def main():
 66    q=.0025; d=.9; initial=[Comp(1,0,.01)]
 67    # Prediction 1: after one step, mode separation is 2d, independent of covariance.
 68    sep=[]
 69    for dd in [.1,.3,.6,.9,1.2]:
 70        z=propagate(initial,dd,q)
 71        sep.append({'d':dd,'observed':z[1].m-z[0].m,'predicted':2*dd})
 72    # Prediction 2: two equal Gaussian modes merge precisely at W2 tolerance >= separation.
 73    threshold_sweep=[]
 74    raw=propagate(initial,d,q)
 75    rawdist=gaussian_w2(raw[0],raw[1])
 76    for eps in [.2,.9,1.5,1.79,1.8,1.81,2.0]:
 77        threshold_sweep.append({'eps':eps,'components':len(merge(raw,eps)),'pair_w2':rawdist,'predicted_merge':eps >= rawdist})
 78    # Prediction 3: merging at tolerance above pair W2 changes the represented law,
 79    # while tolerances below it preserve both modes.
 80    merge_error = []
 81    rg = np.random.default_rng(SEED + 77)
 82    reference = sample(raw, 30000, rg)
 83    for eps in [.9, 1.79, 1.81, 2.0]:
 84        mc = merge(raw, eps)
 85        approx = sample(mc, 30000, np.random.default_rng(SEED + int(100*eps)))
 86        merge_error.append({'eps':eps,'components':len(mc),'sample_w2_to_unmerged':empirical_w2(reference,approx),
 87                            'predicted_merged':eps >= rawdist})
 88    # Prediction 4: before merging, chance probability is mixture CDF and differs from moment Gaussian.
 89    # A symmetric threshold at zero is exactly 0.5; use a positive threshold for a quantitative sweep.
 90    chance_rows=[]
 91    for dd in [.1,.3,.6,.9,1.2]:
 92        z=propagate(initial,dd,q); mm=gaussian_component(z)
 93        p_mix=chance(z,.5); p_gauss=chance(mm,.5)
 94        chance_rows.append({'d':dd,'mixture':p_mix,'moment_gaussian':p_gauss,'absolute_gap':abs(p_mix-p_gauss)})
 95    # Small nonlinear rollout comparison against moment matching, with exact mixture as reference.
 96    horizons=[1,2,4,6,8]
 97    rollout=[]
 98    for H in horizons:
 99        exact=initial
100        mix=initial
101        for _ in range(H):
102            exact=propagate(exact,d,q)
103            mix=propagate(mix,d,q,eps=1e9) # one component after every step
104        rg=np.random.default_rng(SEED+H)
105        a=sample(exact,20000,rg); b=sample(mix,20000,rg)
106        rollout.append({'horizon':H,'components_exact':len(exact),'components_gaussian':len(mix),
107                        'sample_w2_error_gaussian_vs_mixture':empirical_w2(a,b),
108                        'exact_mean':moments(exact)[0],'gaussian_mean':moments(mix)[0]})
109    out={'seed':SEED,'parameters':{'d':d,'q':q},'prediction_mode_separation':sep,
110         'prediction_merge_transition':threshold_sweep,'prediction_chance_calibration':chance_rows,'merge_error_sweep':merge_error,
111         'rollout_comparison':rollout}
112    with open('results.json','w') as f: json.dump(out,f,indent=2)
113    print(json.dumps(out,indent=2))
114
115if __name__=='__main__': main()