Power-Preserving Formation GNN / power_formation_gnn.py

Failed on benchmark

Raw ⬇ ZIP
  1"""Minimal numerical test of a power-preserving formation GNN.
  2
  3Run with:
  4  /home/maxwelhelp/main/bin/python3 power_formation_gnn.py
  5"""
  6import json
  7import numpy as np
  8
  9
 10def formation_matrix(n, d, edges):
 11    B = np.zeros((len(edges), n))
 12    for k, (i, j) in enumerate(edges):
 13        B[k, i] = 1.0
 14        B[k, j] = -1.0
 15    return np.kron(B, np.eye(d))
 16
 17
 18def port_hamiltonian_matrices(F, damping_node=0.15, damping_edge=0.15):
 19    nd, me = F.shape
 20    J = np.block([[np.zeros((nd, nd)), -F.T], [F, np.zeros((me, me))]])
 21    R = np.diag(np.r_[np.full(nd, damping_node), np.full(me, damping_edge)])
 22    return J, R
 23
 24
 25def energy(x, mass):
 26    return 0.5 * np.sum(x * x / mass)
 27
 28
 29def verify_math(rng):
 30    F = formation_matrix(4, 2, [(0, 1), (1, 2), (2, 3), (0, 3)])
 31    J, R = port_hamiltonian_matrices(F)
 32    mass = np.ones(J.shape[0])
 33    x = rng.normal(size=J.shape[0])
 34    grad = x / mass
 35    interconnection_power = float(grad @ J @ grad)
 36    dissipation_power = float(grad @ R @ grad)
 37    dx = (J - R) @ grad
 38    analytic_dh = float(grad @ dx)
 39    eps = 1e-7
 40    finite_difference = (energy(x + eps * dx, mass) - energy(x, mass)) / eps
 41    return {
 42        "skew_error": float(np.max(np.abs(J + J.T))),
 43        "power_cancellation_abs": abs(interconnection_power),
 44        "dissipation_power": dissipation_power,
 45        "analytic_dH": analytic_dh,
 46        "finite_difference_dH": float(finite_difference),
 47        "derivative_match_abs": abs(analytic_dh - finite_difference),
 48    }
 49
 50
 51def simulate_ph(J, R, x0, dt, steps):
 52    x = x0.copy()
 53    hist = [energy(x, np.ones_like(x))]
 54    for _ in range(steps):
 55        x = x + dt * ((J - R) @ x)
 56        hist.append(energy(x, np.ones_like(x)))
 57    return np.asarray(hist), x
 58
 59
 60def stability_sweep(rng):
 61    F = formation_matrix(4, 2, [(0, 1), (1, 2), (2, 3), (0, 3)])
 62    J, R = port_hamiltonian_matrices(F, 0.2, 0.2)
 63    x0 = rng.normal(size=J.shape[0])
 64    stated_bound = 2.0 / np.linalg.eigvalsh(R).max()
 65    # Actual linear explicit-Euler condition is rho(I + dt(J-R)) <= 1.
 66    A = J - R
 67    eig = np.linalg.eigvals(A)
 68    actual_limit = np.inf
 69    for lam in eig:
 70        if lam.real < 0:
 71            candidate = -2.0 * lam.real / (abs(lam) ** 2)
 72            actual_limit = min(actual_limit, candidate)
 73    dts = [0.001, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 10.0]
 74    records = []
 75    for dt in dts:
 76        h, _ = simulate_ph(J, R, x0, dt, 100)
 77        records.append({
 78            "dt": dt,
 79            "spectral_radius": float(max(abs(1.0 + dt * eig))),
 80            "final_energy": float(h[-1]),
 81            "max_energy": float(h.max()),
 82        })
 83    return {
 84        "stated_damping_only_bound": float(stated_bound),
 85        "actual_linear_euler_limit": float(actual_limit),
 86        "records": records,
 87    }
 88
 89
 90def run_depth_experiment(rng):
 91    n, d = 12, 2
 92    edges = [(i, i + 1) for i in range(n - 1)] + [(0, n - 1)]
 93    F = formation_matrix(n, d, edges)
 94    J, R = port_hamiltonian_matrices(F, damping_node=0.12, damping_edge=0.12)
 95    x0 = rng.normal(size=J.shape[0])
 96    node0 = x0[: n * d]
 97    L = F.T @ F
 98    A = np.eye(n * d) - L / max(np.linalg.eigvalsh(L).max(), 1e-12)
 99    unconstrained = node0.copy()
100    ph = x0.copy()
101    rows = []
102    for k in range(51):
103        rows.append({
104            "step": k,
105            "unconstrained_node_norm": float(np.linalg.norm(unconstrained)),
106            "ph_total_energy": float(energy(ph, np.ones_like(ph))),
107            "ph_node_norm": float(np.linalg.norm(ph[: n * d])),
108        })
109        unconstrained = 1.18 * (A @ unconstrained)
110        ph = ph + 0.08 * ((J - R) @ ph)
111    return rows
112
113
114def main():
115    rng = np.random.default_rng(3165)
116    math = verify_math(rng)
117    stability = stability_sweep(rng)
118    depth = run_depth_experiment(rng)
119    out = {
120        "math": math,
121        "stability": stability,
122        "depth_summary": {
123            "unconstrained_norm_step0": depth[0]["unconstrained_node_norm"],
124            "unconstrained_norm_step50": depth[-1]["unconstrained_node_norm"],
125            "ph_energy_step0": depth[0]["ph_total_energy"],
126            "ph_energy_step50": depth[-1]["ph_total_energy"],
127            "ph_max_energy": max(x["ph_total_energy"] for x in depth),
128            "ph_node_norm_step0": depth[0]["ph_node_norm"],
129            "ph_node_norm_step50": depth[-1]["ph_node_norm"],
130        },
131    }
132    with open("results.json", "w") as f:
133        json.dump(out, f, indent=2)
134    print(json.dumps(out, indent=2))
135
136
137if __name__ == "__main__":
138    main()