Hidden-Diffusion Irreversibility Monitor / hidden_diffusion_monitor.py

Failed on benchmark

Raw ⬇ ZIP
  1import numpy as np
  2from scipy.linalg import expm, solve_continuous_lyapunov
  3
  4
  5def stationary_cov(A, D):
  6    # A C + C A^T = 2D for dX=-A X dt + sqrt(2D)dW
  7    return -solve_continuous_lyapunov(A, -2.0 * D)
  8
  9
 10def exact_step(A, D, dt):
 11    C = stationary_cov(A, D)
 12    F = expm(-A * dt)
 13    Q = (C - F @ C @ F.T)
 14    Q = (Q + Q.T) / 2
 15    vals, vecs = np.linalg.eigh(Q)
 16    Q = vecs @ np.diag(np.maximum(vals, 1e-12)) @ vecs.T
 17    return C, F, Q
 18
 19
 20def simulate(A, D, dt, n, rng):
 21    C, F, Q = exact_step(A, D, dt)
 22    out = np.empty((n, 2))
 23    out[0] = rng.multivariate_normal(np.zeros(2), C)
 24    noise = rng.multivariate_normal(np.zeros(2), Q, size=n - 1)
 25    for k in range(n - 1):
 26        out[k + 1] = F @ out[k] + noise[k]
 27    return out, C, F, Q
 28
 29
 30def gaussian_logpdf(v, cov):
 31    sign, logdet = np.linalg.slogdet(cov)
 32    inv = np.linalg.inv(cov)
 33    return -.5 * (logdet + v @ inv @ v + 2*np.log(2*np.pi))
 34
 35
 36def forward_reverse_action_gap(path, F, Q, dt):
 37    """Discretized path-action gap using the forward transition both ways.
 38    This is the standard forward path versus its time-reversed ordering;
 39    unlike a fitted conditional-entropy difference it detects probability currents.
 40    """
 41    vals = []
 42    for x, y in zip(path[:-1], path[1:]):
 43        vals.append(gaussian_logpdf(y - F @ x, Q) -
 44                    gaussian_logpdf(x - F @ y, Q))
 45    return float(np.mean(vals) / dt)
 46
 47
 48def analytic_spectrum(omega, A, D):
 49    out = np.empty_like(omega, dtype=float)
 50    for i, w in enumerate(omega):
 51        H = np.linalg.inv(A + 1j * w * np.eye(2))
 52        out[i] = np.real(2 * (H @ D @ H.conj().T)[0, 0])
 53    return out
 54
 55
 56def paper_sigma(a, b, Dx, Dm, Dc):
 57    return (a * (Dc + Dm) + b * Dx) ** 2 / (a * (Dx * Dm - Dc * Dc))
 58
 59
 60def autocorr(path, maxlag):
 61    z = path[:, 0] - path[:, 0].mean()
 62    den = np.dot(z, z)
 63    return np.array([np.dot(z[:-k] if k else z, z[k:] if k else z) /
 64                     (den if k == 0 else np.sqrt(np.dot(z[:-k], z[:-k]) * np.dot(z[k:], z[k:])))
 65                     for k in range(maxlag + 1)])
 66
 67
 68def main():
 69    rng = np.random.default_rng(20250308)
 70    # Exact adaptation: A_yy=0 and nontrivial coupling. Stable since det(A)>0.
 71    a, b, g = 1.0, 1.0, -1.0
 72    A = np.array([[a, b], [g, 0.0]])
 73    Dx = Dm = 1.0
 74    dcs = [-0.75, 0.0, 0.75]
 75    omega = np.logspace(-2, 2, 300)
 76    reference = analytic_spectrum(omega, A, np.diag([Dx, Dm]))
 77    print('A=', A.tolist())
 78    print('Analytic blind-direction check (max relative PSD error):')
 79    rows = []
 80    for dc in dcs:
 81        D = np.array([[Dx, dc], [dc, Dm]])
 82        spec = analytic_spectrum(omega, A, D)
 83        rel = np.max(np.abs(spec - reference) / np.maximum(reference, 1e-12))
 84        predicted = paper_sigma(a, b, Dx, Dm, dc)
 85        path, C, F, Q = simulate(A, D, dt=.03, n=180000, rng=rng)
 86        gap = forward_reverse_action_gap(path, F, Q, .03)
 87        # Compare observed autocorrelation to its exact stationary prediction.
 88        lags = np.arange(1, 101) * .03
 89        exact_ac = np.array([(expm(-A*t) @ C)[0, 0] / C[0, 0] for t in lags])
 90        sample_ac = autocorr(path, 100)[1:]
 91        ac_err = float(np.max(np.abs(sample_ac - exact_ac)))
 92        rows.append((dc, rel, predicted, gap, ac_err))
 93    sigmas = np.array([r[2] for r in rows]); gaps = np.array([r[3] for r in rows])
 94    print(' dc     analytic_rel_err    sigma_formula    action_gap       max_AC_error')
 95    for r in rows:
 96        print('% .2f   % .3e          % .6f       % .6f        % .4f' % r)
 97    print('formula sigma range:', float(sigmas.min()), float(sigmas.max()))
 98    print('action-gap range:', float(gaps.min()), float(gaps.max()))
 99    print('spectrum remains blind:', max(r[1] for r in rows) < 1e-10)
100    print('autocorrelation agreement:', max(r[4] for r in rows) < .03)
101    print('gap spread / formula spread:', float(np.ptp(gaps)), float(np.ptp(sigmas)))
102
103if __name__ == '__main__':
104    main()