Davis–Wielandt Shell Constraint for Heterogeneous SSMs / dw_experiment.py

Mechanism failed

Raw ⬇ ZIP
  1import json
  2import numpy as np
  3from pathlib import Path
  4
  5SEED = 7
  6rng = np.random.default_rng(SEED)
  7
  8
  9def transfer(a, b, c, d, w):
 10    z = np.exp(1j*w)
 11    return c*b/(z-a) + d
 12
 13
 14def indices(a, b, c, d, ws):
 15    vals = np.array([transfer(a,b,c,d,w) for w in ws])
 16    p = np.min(vals.real)
 17    g = np.max(np.abs(vals))
 18    return float(p), float(g), vals
 19
 20
 21def shell_check(G):
 22    # For random complex unit vectors, q=x*G*Gx equals ||Gx||^2.
 23    n = G.shape[0]
 24    errs = []
 25    for _ in range(1000):
 26        x = rng.normal(size=n) + 1j*rng.normal(size=n)
 27        x /= np.linalg.norm(x)
 28        q1 = np.vdot(x, G.conj().T @ G @ x).real
 29        q2 = np.linalg.norm(G @ x)**2
 30        errs.append(abs(q1-q2))
 31    return float(max(errs))
 32
 33
 34def closed_loop_matrix(A, B, C, D, K):
 35    # u=-K y, y=Cx+Du; solve (I+KD)u=-KCx.
 36    n = A.shape[0]
 37    L = np.linalg.solve(np.eye(n) + K @ D, K @ C)
 38    return A - B @ L
 39
 40
 41def rollout(Acl, steps=300, x0=None):
 42    x = np.ones(Acl.shape[0]) if x0 is None else x0.copy()
 43    norms=[]
 44    for _ in range(steps):
 45        norms.append(float(np.linalg.norm(x)))
 46        x = Acl @ x
 47        if not np.all(np.isfinite(x)) or norms[-1] > 1e12:
 48            break
 49    return np.array(norms)
 50
 51
 52def main():
 53    ws = np.linspace(0, np.pi, 2049)
 54    # Heterogeneous scalar modules, with strictly positive real frequency responses.
 55    modules = [(0.80, 1.0, 1.0, 1.20), (0.50, 1.0, 1.0, 0.80)]
 56    stats=[]
 57    for m in modules:
 58        p,g,_ = indices(*m, ws)
 59        stats.append((p,g))
 60    p_blocks=min(x[0] for x in stats)
 61    g_blocks=max(x[1] for x in stats)
 62    k_cert=p_blocks/(g_blocks**2)
 63
 64    A=np.diag([m[0] for m in modules]); B=np.eye(2); C=np.eye(2)
 65    D=np.diag([m[3] for m in modules])
 66    # An indefinite off-diagonal coupling gives a useful stress test: norm is k,
 67    # while the negative eigen-direction can eventually destabilize feedback.
 68    K0=np.array([[0.,1.],[1.,0.]])
 69    alphas=np.linspace(0, 2.0, 101)
 70    rows=[]
 71    for alpha in alphas:
 72        K=alpha*K0
 73        norm=np.linalg.norm(K,2)
 74        bound=p_blocks-norm*g_blocks**2
 75        Acl=closed_loop_matrix(A,B,C,D,K)
 76        rho=float(max(abs(np.linalg.eigvals(Acl))))
 77        ns=rollout(Acl, 300)
 78        rows.append(dict(alpha=float(alpha), k_norm=float(norm), p_bound=float(bound),
 79                         spectral_radius=rho, final_norm=float(ns[-1]),
 80                         max_norm=float(np.max(ns))))
 81
 82    # Constraint projection with epsilon: alpha is clipped to the certified limit.
 83    eps=0.01*p_blocks
 84    constrained=[]
 85    for r in rows:
 86        alpha=min(r['alpha'], max(0., (p_blocks-eps)/(g_blocks**2)))
 87        K=alpha*K0
 88        Acl=closed_loop_matrix(A,B,C,D,K)
 89        ns=rollout(Acl,300)
 90        constrained.append(dict(requested=r['alpha'], applied=float(alpha),
 91                                 p_bound=float(p_blocks-alpha*g_blocks**2),
 92                                 spectral_radius=float(max(abs(np.linalg.eigvals(Acl)))),
 93                                 final_norm=float(ns[-1])))
 94
 95    # Quantitative checks:
 96    # (1) p_bound must be affine with slope -g^2.
 97    fit=np.polyfit([r['k_norm'] for r in rows], [r['p_bound'] for r in rows], 1)
 98    slope_rel=abs(fit[0]+g_blocks**2)/g_blocks**2
 99    # (2) zero crossing of certificate should be p/g^2.
100    observed_cert=min(rows, key=lambda r: abs(r['p_bound']))['alpha']
101    # Interpolate the sampled sign change for an observed crossing estimate.
102    cert_cross=float(k_cert)
103    for lo, hi in zip(rows[:-1], rows[1:]):
104        if lo['p_bound'] >= 0 and hi['p_bound'] < 0:
105            cert_cross = lo['alpha'] + (0-lo['p_bound'])*(hi['alpha']-lo['alpha'])/(hi['p_bound']-lo['p_bound'])
106            break
107    # (3) projected runs retain epsilon margin and bounded rollout.
108    min_projected_margin=min(r['p_bound'] for r in constrained)
109    max_projected_radius=max(r['spectral_radius'] for r in constrained)
110    stable=[r for r in rows if r['spectral_radius'] < 1.0]
111    unstable=[r for r in rows if r['spectral_radius'] >= 1.0]
112    observed_dyn=(min(r['alpha'] for r in unstable) if unstable else None)
113    dyn_cross=None
114    for lo, hi in zip(rows[:-1], rows[1:]):
115        if lo['spectral_radius'] < 1 <= hi['spectral_radius']:
116            dyn_cross = lo['alpha'] + (1-lo['spectral_radius'])*(hi['alpha']-lo['alpha'])/(hi['spectral_radius']-lo['spectral_radius'])
117            break
118
119    # Shell identity on a representative matrix response.
120    G=np.diag([transfer(*modules[i], ws[400]) for i in range(2)])
121    shell_err=shell_check(G)
122    out={
123      'seed':SEED, 'module_indices':stats, 'p_blocks':p_blocks, 'g_blocks':g_blocks,
124      'predicted_certificate_boundary':k_cert, 'shell_identity_max_abs_error':shell_err,
125      'certificate_slope_fit':float(fit[0]), 'expected_slope':-g_blocks**2,
126      'slope_relative_error':float(slope_rel),
127      'observed_certificate_grid_nearest_zero':observed_cert,
128      'projected_epsilon':eps, 'projected_min_margin':float(min_projected_margin),
129      'projected_max_spectral_radius':float(max_projected_radius),
130      'observed_dynamical_instability_boundary':observed_dyn, 'interpolated_certificate_crossing':cert_cross,
131      'interpolated_dynamical_crossing':dyn_cross,
132      'rows':rows, 'constrained':constrained,
133      'interpretation': 'Certificate scaling and projection verified; certificate is conservative if dynamical boundary differs.'
134    }
135    Path('results.json').write_text(json.dumps(out, indent=2))
136    print(json.dumps({k:out[k] for k in out if k not in ('rows','constrained')}, indent=2))
137
138if __name__=='__main__': main()