In [1]:
# Import useful libraries
import numpy as np
import scipy as sc
import networkx as nx
import pandas as pd


n = 10
d = 3
T = 250
p = 3
beta = np.array([2, -1, 3])
sigma2 = 1

# Set a seed if desired
seed = 2025
rng = np.random.default_rng(seed = seed)
In [2]:
def build_graph(d, n, seed):

    g = nx.random_regular_graph(d, n, seed)
    W = nx.to_numpy_array(g)

    evals, evecs = np.linalg.eigh(W)

    hW = sc.linalg.expm(W)
    Ws = np.array([
        np.outer(evecs[:, i], np.linalg.inv(evecs)[i, :]) for i in range(n)
    ])

    print("Evec version is Idempotent:", np.allclose(Ws[0], Ws[0] @ Ws[0]))


    return evals, Ws, hW
In [3]:
def generate_linear_component(T, n, p, beta, sigma2, rng):
    X = np.concatenate([
    np.ones((T, n, 1)),
    rng.normal(size = (T, n, p - 1))
        ], axis = -1)
    beta = np.array([2, -1, 3])

    sigma2 = 1
    e = np.sqrt(sigma2) * rng.normal(size = (T, n))

    # Linear component
    eta = X @ beta + e

    return X, eta
In [4]:
def transform_linear_component(eta, hW):
    Y = np.einsum("ij,hj", np.linalg.inv(hW), eta)

    return Y
In [5]:
def computation(X, n, T):
    
    X_stack = X.reshape(-1, 3)
    A_stack = np.linalg.inv(X_stack.T @ X_stack) @ X_stack.T
    M_stack = np.eye(T * n) - X_stack @ np.linalg.inv(X_stack.T @ X_stack) @ X_stack.T

    return A_stack, M_stack
In [6]:
def estimate_kappa(Y, Ws, T, n, A_stack, M_stack):

    def loss(kappa_1p_hat):
        kappa_hat = np.concatenate([[1], kappa_1p_hat], axis = 0)

        # Slow method:
        # hY_hat = np.zeros((T, n))
        # for t in range(Y.shape[0]):
        #     for i in range(kappa_hat.shape[0]):
        #         hY_hat[t, :] += kappa_hat[i] * (Ws[i] @ Y[t])

        # Fast method
        hY_hat = np.einsum("i,ijk,hk", kappa_hat, Ws, Y)
        hY_hat_stack = hY_hat.reshape(-1)

        # Compute log determinant term
        S_kappa_hat = np.sum([
            kappa_hat[i] * Ws[i] for i in range(n)
        ], axis = 0)
        sign, abslogdet = np.linalg.slogdet(S_kappa_hat)
        logdet = sign * abslogdet

        # Compute some garbage
        beta_hat = A_stack @ hY_hat_stack
        sigma2_hat = np.mean(np.square(M_stack @ hY_hat_stack))

        # Concentrated log likelihood
        L = -0.5 * n * T * np.log(sigma2_hat) + T * logdet

        return -L

    kappa_1p_init = np.ones(n - 1)
    res = sc.optimize.minimize(loss, x0 = kappa_1p_init)

    return np.concatenate([[1], res.x])
In [7]:
#choose number of simulations

num_sims = 250
In [8]:
evals, Ws, hW = build_graph(d, n, seed)
true_kappa = np.exp(evals) / np.exp(evals[0])
estimates = []

for sim in range(num_sims):
    X, eta = generate_linear_component(T, n, p, beta, sigma2, rng)
    Y = transform_linear_component(eta, hW)
    A_stack, M_stack = computation(X, n, T)
    estimated_kappa = estimate_kappa(Y, Ws, T, n, A_stack, M_stack)
    estimates.append(estimated_kappa)

# average the estimated kappa
estimates = np.array(estimates)
mean_kappa = estimates.mean(axis=0)

print("True kappa:", np.exp(evals) / np.exp(evals[0]))
print("estimated Kappa average: ", mean_kappa)
Evec version is Idempotent: True
True kappa: [  1.           1.57805456   2.3417808    4.28959703   8.06781785
  11.66033367  31.69607313  42.06966776  99.76747962 234.20406245]
estimated Kappa average:  [  1.           1.57456493   2.34271076   4.29437674   8.0650336
  11.67029994  31.68573327  42.07039518  99.7791601  233.47800453]