Noise-Triggered Latent Rank Adaptation / experiment.py

Mechanism confirmed, baseline not beaten

Raw ⬇ ZIP
  1import json, math
  2from pathlib import Path
  3import numpy as np
  4
  5SEED = 1049
  6
  7class RankController:
  8    def __init__(self, dmax, alpha=0.08, con=2.0, coff=1.2, M=3, cooldown=0):
  9        self.dmax, self.alpha, self.con, self.coff, self.M = dmax, alpha, con, coff, M
 10        self.C = np.zeros((dmax, dmax)); self.active = np.zeros(dmax, dtype=bool)
 11        self.above = np.zeros(dmax, dtype=int); self.below = np.zeros(dmax, dtype=int)
 12        self.cooldown = cooldown; self.cool = 0; self.events = []
 13    def update(self, y, noise_var):
 14        self.C = (1-self.alpha)*self.C + self.alpha*np.outer(y, y)
 15        S = (self.C - noise_var*np.eye(self.dmax) + (self.C - noise_var*np.eye(self.dmax)).T)/2
 16        vals, vecs = np.linalg.eigh(S); vals = vals[::-1]; vecs = vecs[:, ::-1]
 17        # In this diagonal toy, sorting is equivalent to ordered subspace eigenvalues.
 18        on = vals > self.con*noise_var; off = vals < self.coff*noise_var
 19        if self.cool: self.cool -= 1
 20        for i in range(self.dmax):
 21            if on[i]: self.above[i] += 1
 22            else: self.above[i] = 0
 23            if off[i]: self.below[i] += 1
 24            else: self.below[i] = 0
 25        if not self.cool:
 26            for i in range(self.dmax):
 27                if (not self.active[i]) and self.above[i] >= self.M:
 28                    self.active[i] = True; self.events.append((len(self.events), 'on', i)); self.cool=self.cooldown
 29                elif self.active[i] and self.below[i] >= self.M:
 30                    self.active[i] = False; self.events.append((len(self.events), 'off', i)); self.cool=self.cooldown
 31        return vals, self.active.copy()
 32
 33def crossing_sweep():
 34    # Prediction: stationary corrected population eigenvalue is signal variance s;
 35    # activation should occur iff s > c_on*sigma^2, approximately at ratio c_on.
 36    rng=np.random.default_rng(SEED); alpha=.08; con=2.; coff=1.2; M=3
 37    ratios=np.array([0.5,1.,1.5,1.9,2.0,2.1,2.5,4.])
 38    outcomes=[]
 39    for q in ratios:
 40        hits=[]
 41        for rep in range(20):
 42            ctl=RankController(1,alpha,con,coff,M)
 43            for _ in range(500): ctl.update(np.array([rng.normal(0, math.sqrt(q)) + rng.normal()]),1.)
 44            hits.append(bool(ctl.active[0]))
 45        outcomes.append(float(np.mean(hits)))
 46    # Estimate transition by interpolating the 50% point across a dense sweep.
 47    dense=np.linspace(.5,4,36); probs=[]
 48    for q in dense:
 49        h=[]
 50        for rep in range(12):
 51            ctl=RankController(1,alpha,con,coff,M)
 52            for _ in range(400): ctl.update(np.array([rng.normal(0,math.sqrt(q))+rng.normal()]),1.)
 53            h.append(ctl.active[0])
 54        probs.append(np.mean(h))
 55    trans=float(dense[np.argmin(np.abs(np.array(probs)-.5))])
 56    # Deterministic population check: corrected covariance is exactly q, so crossing is q > c_on.
 57    deterministic = []
 58    for q in ratios:
 59        ctl=RankController(1,alpha,con,coff,M)
 60        # Feed a constant-magnitude sequence whose sample variance is q+noise.
 61        for t in range(200):
 62            y=np.array([math.sqrt(q+1.) if t%2==0 else -math.sqrt(q+1.)])
 63            ctl.update(y,1.)
 64        deterministic.append(bool(ctl.active[0]))
 65    return {'predicted_ratio':con,'swept_ratios':ratios.tolist(),'activation_probability':outcomes,'observed_50pct_ratio':trans,'deterministic_activation':deterministic,'deterministic_boundary_between': [float(ratios[i]) for i in range(len(ratios)-1) if deterministic[i]!=deterministic[i+1]]}
 66
 67def delay_sweep():
 68    # After an abrupt jump, EWMA expectation is C_t = s_new+(C0-s_new)(1-a)^t.
 69    # Crossing delay is the first t with C_t-noise > con*noise.
 70    rng=np.random.default_rng(SEED+1); alpha=.1; noise=1.; con=2.; M=1
 71    s0=.2; s1=5.; target=(1+con)
 72    predicted=math.ceil(math.log((target-s1)/(s0+1-s1))/math.log(1-alpha))
 73    delays=[]
 74    for rep in range(30):
 75        ctl=RankController(1,alpha,con,1.2,M)
 76        for _ in range(100): ctl.update(np.array([rng.normal(0,math.sqrt(s0))+rng.normal()]),noise)
 77        delay=None
 78        for t in range(1,150):
 79            _,a=ctl.update(np.array([rng.normal(0,math.sqrt(s1))+rng.normal()]),noise)
 80            if a[0]: delay=t; break
 81        delays.append(delay if delay is not None else 150)
 82    return {'predicted_first_crossing_steps':predicted,'observed_median_steps':float(np.median(delays)),'observed_mean_steps':float(np.mean(delays)),'delays':delays}
 83
 84def chatter_test():
 85    # Near-threshold noisy signal: hysteresis should reduce toggles relative to one threshold.
 86    rng=np.random.default_rng(SEED+2); n=3000
 87    def run(coff):
 88        ctl=RankController(1,.12,2.,coff,3)
 89        toggles=0; prev=False
 90        for _ in range(n):
 91            _,a=ctl.update(np.array([rng.normal(0,math.sqrt(1.9))+rng.normal()]),1.)
 92            toggles += int(a[0] != prev); prev=bool(a[0])
 93        return toggles, len(ctl.events), bool(prev)
 94    # same random distribution, reset stream for fair comparison
 95    rng=np.random.default_rng(SEED+2); h=run(1.2)
 96    rng=np.random.default_rng(SEED+2); no=run(2.0)
 97    return {'hysteresis_coff_1.2':h,'single_threshold_coff_2.0':no,'toggle_reduction_fraction':1-h[0]/max(no[0],1)}
 98
 99def switched_rank_demo():
100    # Minimal order-adaptive reconstruction: observations are independent latent channels + sensor noise.
101    # Fixed rank-2, fixed rank-5, and controller rank; report MSE on clean latent reconstruction.
102    rng=np.random.default_rng(SEED+3); d=5; T=2400; noise=.35
103    ys=[]; xs=[]; true=[]
104    for t in range(T):
105        r=2 if t<700 or t>=1700 else 5
106        x=np.zeros(d); x[:r]=rng.normal(size=r)
107        y=x+rng.normal(0,noise,size=d); ys.append(y); xs.append(x); true.append(r)
108    ys=np.array(ys); xs=np.array(xs)
109    ctl=RankController(d,.1,2.,1.2,3); pred=[]; ranks=[]
110    for y in ys:
111        _,a=ctl.update(y,noise**2); ranks.append(int(a.sum())); pred.append(y*a)
112    # oracle fixed masks are idealized standard fixed-width references
113    mse_ad=float(np.mean((np.array(pred)-xs)**2))
114    mse2=float(np.mean((ys*np.array([1,1,0,0,0])-xs)**2))
115    mse5=float(np.mean((ys-xs)**2))
116    return {'adaptive_mse':mse_ad,'fixed_rank2_mse':mse2,'fixed_rank5_mse':mse5,'mean_active_rank':float(np.mean(ranks)),'segment_mean_ranks':[float(np.mean(ranks[:700])),float(np.mean(ranks[700:1700])),float(np.mean(ranks[1700:]))], 'segment_mse_adaptive':[float(np.mean((np.array(pred[:700])-xs[:700])**2)),float(np.mean((np.array(pred[700:1700])-xs[700:1700])**2)),float(np.mean((np.array(pred[1700:])-xs[1700:])**2))]}
117
118def main():
119    out={'seed':SEED,'crossing_sweep':crossing_sweep(),'ewma_delay':delay_sweep(),'hysteresis':chatter_test(),'switched_demo':switched_rank_demo()}
120    Path('results.json').write_text(json.dumps(out,indent=2))
121    print(json.dumps(out,indent=2))
122if __name__=='__main__': main()