{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0f9022b6",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sage.all import *\n",
    "import numpy as np\n",
    "from matplotlib import pyplot as plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "e9570b19",
   "metadata": {},
   "outputs": [],
   "source": [
    "class NBRW:\n",
    "    \"\"\"A class to store several relevant attributes of a graph G in Non-Backtracking Random Walks\"\"\"\n",
    "    \n",
    "    def __init__(self, G, pinwheel=False):\n",
    "        \"\"\"Accepts a a sage graph\"\"\"\n",
    "        \n",
    "        self.G = G  # store graph\n",
    "        self.m = len(G.edges())  # number of edges\n",
    "        self.n = len(G.vertices())  # number of vertices\n",
    "        \n",
    "        self.S = self.S_mat()\n",
    "        self.T = self.T_mat()\n",
    "        self.tau = self.tau_mat()\n",
    "        self.B = self.B_mat()\n",
    "        self.Dinv = self.D_inv(pinwheel)\n",
    "        \n",
    "        self.Pnb = self.nb_trans_mat()\n",
    "        self.Znb = self.Znb_mat()\n",
    "        self.Wnb = self.Wnb_mat()\n",
    "        self.Mev = self.M_ev_NBRW()\n",
    "        self.Znb_e = self.Znb_e_mat()\n",
    "        self.M = self.M_mat()\n",
    "        self.Mv_nb = self.nb_vertex_mfpt()\n",
    "        self.Wv = self.Wv_mat()\n",
    "        self.Rv = self.mrt_NBRW_vertex()\n",
    "        self.Knb_v = self.nb_vertex_kemeny_mfpt(pinwheel)\n",
    "        self.P = self.trans_mat_sage()\n",
    "        self.K_vertex = self.kemeny_vertex()\n",
    "        self.K_edge = self.kemeney_edge()\n",
    "        self.Mnb_e = self.nb_mfpt_mat()\n",
    "        self.Knb_e = self.kemeney_nb_edge()\n",
    "\n",
    "        self.De = self.De_mat()\n",
    "        self.De_inv = np.linalg.inv(self.De)\n",
    "        self.Pe = self.De_inv @ self.S @ self.T\n",
    "        self.Re = self.Re_mat()\n",
    "        \n",
    "        self.Knb_v2 = np.trace(self.Znb) - 1\n",
    "        self.Knb_v3 = self.nb_vertex_kemeny_trace()\n",
    "\n",
    "        self.volG = 2 * len(self.G.edges())\n",
    "        if pinwheel:\n",
    "            self.pi = np.array([d / self.volG for d in sorted(self.G.degree(), reverse=True)])\n",
    "        else:\n",
    "            self.pi = np.array([d / self.volG for d in self.G.degree()])\n",
    "\n",
    "        self.Z = self.fund_mat()\n",
    "        self.Mv = self.mfpt_mat(size=self.n, Z=self.Z, pi=self.pi)\n",
    "\n",
    "        self.Italian_Mv_nb = self.italian_hitting_times_mat()\n",
    "\n",
    "        # New Edge Space Stuff\n",
    "        self.pi_e = np.ones(2*self.m) / (2*self.m)\n",
    "        self.We = np.outer(np.ones(2*self.m), self.pi_e)\n",
    "        self.Z_e = np.linalg.inv(np.eye(2*self.m) - self.Pe + self.We)\n",
    "        self.M_e = self.mfpt_mat(size=2*self.m, Z=self.Z_e, pi=self.pi_e)\n",
    "\n",
    "        \n",
    "    \n",
    "    def show(self):\n",
    "        \"\"\"Prints the graph\"\"\"\n",
    "        self.G.show()\n",
    "    \n",
    "    def S_mat(self):\n",
    "        \"\"\"Returns matrix S such that ST = C (TS=A)\n",
    "        S maps from edge space to vertex space\n",
    "        S is a 2m x n matrix\"\"\"\n",
    "        \n",
    "        edges = list(self.G.edges())\n",
    "        \n",
    "        S = matrix.zero(2*self.m, self.n)\n",
    "        for x in range(self.n):\n",
    "            # iterate through nodes\n",
    "            for j in range(self.m):\n",
    "                # iterate through edges\n",
    "                if edges[j][1] == x:\n",
    "                    S[j, x] = 1\n",
    "                if edges[j][0] == x:\n",
    "                    S[j+self.m, x] = 1\n",
    "        return S\n",
    "    \n",
    "    def T_mat(self):\n",
    "        \"\"\"Returns T matrix such that ST = C (TS=A)\n",
    "        Maps from vertex to edge\n",
    "        n x 2m\"\"\"\n",
    "        \n",
    "        edges = list(self.G.edges())\n",
    "        \n",
    "        T = matrix.zero(self.n, 2*self.m)\n",
    "        for x in range(self.n):\n",
    "            # iterate through nodes\n",
    "            for j in range(self.m):\n",
    "                # iterate through edges\n",
    "                if edges[j][0] == x:\n",
    "                    T[x,j] = 1\n",
    "                if edges[j][1] == x:\n",
    "                    T[x, j+self.m] = 1\n",
    "        \n",
    "        return T\n",
    "    \n",
    "    def tau_mat(self):\n",
    "        \"\"\"Return matrix tau such that ST - tau = B\n",
    "        2m x 2m\n",
    "        Switches directed edges\n",
    "        \"\"\"\n",
    "        \n",
    "        edges = list(self.G.edges())\n",
    "        \n",
    "        zero = matrix.zero(self.m)  # m x m zeros\n",
    "        I = identity_matrix(self.m)  # m x m identity\n",
    "        \n",
    "        t = block_matrix([[zero, I], [I, zero]])\n",
    "        \n",
    "        return t  # like an identity with opposite diagonal\n",
    "    \n",
    "    def B_mat(self):\n",
    "        \"\"\"Return matrix B = ST - tau\n",
    "        non-backtracking edge adjacency matrix\"\"\"\n",
    "        \n",
    "        return self.S * self.T - self.tau\n",
    "    \n",
    "    def nb_trans_mat(self):\n",
    "        \"\"\"return NBRW edge space transition probability matrix\n",
    "        P_{nb} = (D-I)^{-1}B\"\"\"\n",
    "        b = self.B\n",
    "        row_sums = sum(b)\n",
    "        row_sums = [i ^ -1 for i in row_sums]\n",
    "        return b * diagonal_matrix(row_sums)\n",
    "    \n",
    "    def D_inv(self, pinwheel=False):\n",
    "        \"\"\"D^{-1}\"\"\"\n",
    "        if pinwheel:\n",
    "            return np.diag([1 / d for d in sorted(self.G.degree(), reverse=True)])\n",
    "        else:    \n",
    "            return np.diag([1 / d for d in self.G.degree()])\n",
    "    \n",
    "    def trans(self):\n",
    "        \"\"\"vertex space transition matrix of a simple random walk\"\"\"\n",
    "        D = self.D\n",
    "        return D @ self.G.adjacency_matrix()\n",
    "    \n",
    "    def nb_hitting_times(self, j):\n",
    "        \"\"\"\n",
    "        return list (np.array()) m_{i, j} where m is\n",
    "        NB hitting time from i to j for all i, with j fixed.\n",
    "\n",
    "        THIS IS ACTUALLY I THINK nb hitting time m_{e, j}. 2m vector of hitting times from\n",
    "        the edge e to the vertex j. (SEE Theorem 4.3 I believe.)\n",
    "\n",
    "        i.e. the columns of M_{ev}\n",
    "        \"\"\"\n",
    "        # Ask Adam what's going on here!\n",
    "        m = len(self.G.edges())\n",
    "        n = len(self.G)\n",
    "        P = self.nb_trans_mat()\n",
    "        Gedges = [(g[0], g[1]) for g in self.G.edges()]\n",
    "        Gedges.extend([(g[1], g[0]) for g in self.G.edges()])\n",
    "        entries_to_delete = []\n",
    "\n",
    "        # Find the rows/columns to delete\n",
    "        for i in range(len(Gedges)):\n",
    "            if Gedges[i][0] == j:\n",
    "                entries_to_delete.append(i)\n",
    "        P_new = np.delete(P, entries_to_delete, axis=0)\n",
    "        P_new = np.delete(P_new, entries_to_delete, axis=1)\n",
    "\n",
    "        # Keep a list of what was NOT deleted, to construct the full vector later\n",
    "        remaining_entries = [i for i in range(2 * m)]\n",
    "        for k in entries_to_delete:\n",
    "            remaining_entries.remove(k)\n",
    "\n",
    "        # Solve (I - P)x = 1\n",
    "        ones = np.array([1] * len(P_new))\n",
    "        tau_vec = np.linalg.solve(np.identity(len(P_new)) - P_new, ones)\n",
    "\n",
    "        # recreate full tau_vec, putting the 0's back in where necessary\n",
    "        full_tau = np.array([0.0] * (2 * m))\n",
    "        for i in range(len(remaining_entries)):\n",
    "            full_tau[remaining_entries[i]] = tau_vec[i]\n",
    "\n",
    "        return full_tau\n",
    "    \n",
    "    def M_ev_NBRW(self):\n",
    "        \"\"\"\n",
    "        return the matrix $M_{ev}$, that is, the hitting times (mean first passage times)\n",
    "        of the nonbacktracking random walk\n",
    "        \"\"\"\n",
    "        m = len(self.G.edges())\n",
    "        n = len(self.G)\n",
    "        Mev = np.zeros((2 * m, n))\n",
    "        for i in range(n):\n",
    "            Mvec = self.nb_hitting_times(i)\n",
    "            for j in range(2 * m):\n",
    "                Mev[j][i] = Mvec[j]\n",
    "\n",
    "        return Mev\n",
    "    \n",
    "    def P_hat(self):\n",
    "        \"\"\"\n",
    "        (D-I)^{-1}A.\n",
    "        I should probably not take an inverse since its so easy but whatever. change as needed\n",
    "        \"\"\"\n",
    "        return np.diag([1 / (d - 1) for d in G.degree()]) @ G.adjacency_matrix()\n",
    "\n",
    "\n",
    "    def P_mat(self):\n",
    "        \"\"\"\n",
    "        D^{-1}A\n",
    "        \"\"\"\n",
    "        return np.diag([1 / d for d in G.degree()]) @ G.adjacency_matrix()\n",
    "\n",
    "    \n",
    "    def Znb_mat(self):\n",
    "        \"\"\"\n",
    "        A guess at what Znb should be. Seems to work pretty close to what we want numerically. Matches exactly with\n",
    "        edge transitive graphs. Usually close with all other graphs we've tried.\n",
    "        \"\"\"\n",
    "        n = len(self.G)\n",
    "        m = len(self.G.edges())\n",
    "        Dinv = np.diag([1 / d for d in self.G.degree()])\n",
    "        T = self.T\n",
    "        S = self.S\n",
    "        Wnb = np.ones((2 * m, 2 * m)) / (2 * m)\n",
    "        Wv = np.outer(np.ones(n), [d / (2 * m) for d in self.G.degree()])\n",
    "\n",
    "        return np.eye(n) + Dinv @ T @ (np.linalg.inv(np.eye(2 * m) - self.nb_trans_mat() + Wnb)) @ S - Wv\n",
    "\n",
    "    \n",
    "    def Wnb_mat(self):\n",
    "        return np.ones((2 * self.m, 2 * self.m)) / (2 * self.m)\n",
    "    \n",
    "    \n",
    "    def Znb_e_mat(self):\n",
    "        I = identity_matrix(2*self.m)\n",
    "        return np.linalg.inv(I - self.Pnb + self.Wnb)\n",
    "    \n",
    "    \n",
    "    def M_mat(self):\n",
    "        \"\"\"\n",
    "        Matrix M as in (4.4) in the Hitting Times Paper\n",
    "        (Basically the start point operator divided by the row sums)\n",
    "        \"\"\"\n",
    "        M = self.T.numpy()\n",
    "        # Divide by row sums\n",
    "        return M / M.sum(axis=1, keepdims=1)\n",
    "    \n",
    "    \n",
    "    def nb_vertex_mfpt(self):\n",
    "        \"\"\"\n",
    "        Get the matrix (np.array probably) where M_{i,j} is the mfpt i -> j.\n",
    "        That is, $M_v^{nb}$\n",
    "\n",
    "        For now, assumes vertices are {0, 1, ..., n-1}\n",
    "        \"\"\"\n",
    "        n = self.n\n",
    "        MFPT = np.zeros((n, n))\n",
    "\n",
    "        for i in range(n):\n",
    "            MFPT_col_i = self.M @ self.nb_hitting_times(i)\n",
    "            for j in range(n):\n",
    "                MFPT[j][i] = MFPT_col_i[j]\n",
    "\n",
    "        return MFPT\n",
    "    \n",
    "    def Wv_mat(self):\n",
    "        return np.outer(np.ones(self.n), [d / (2 * self.m) for d in self.G.degree()])\n",
    "    \n",
    "    def mrt_NBRW_vertex(self):\n",
    "        \"\"\"\n",
    "        Diagonal matrix of mean return times of a NBRW.\n",
    "        See Equations below (4.8) in hitting times paper.\n",
    "\n",
    "        Note: This should be the same Mean Return Times as SRW I believe which might just be 1/\\pi_j\n",
    "              if I remember correctly as I'm typing this note\n",
    "        \"\"\"\n",
    "        Dinv = np.diag([1 / d for d in self.G.degree()])\n",
    "        T = self.T\n",
    "        Pnb = self.Pnb\n",
    "        Mev = self.Mev\n",
    "        return np.eye(len(self.G)) + np.diag(np.diag(Dinv @ T @ Pnb @ Mev))\n",
    "    \n",
    "    def trans_mat_sage(self):\n",
    "        \"\"\"vertex space transition matrix of a simple random walk\"\"\"\n",
    "        return self.Dinv @ self.G.adjacency_matrix()\n",
    "    \n",
    "    \n",
    "    def Pv_NBRW(self, k):\n",
    "        \"\"\"\n",
    "        Return the vertex space transition probability matrix for the nonbacktracking random walk for the kth step\n",
    "        \"\"\"\n",
    "        if k == 0:\n",
    "            return np.eye(len(self.G))\n",
    "\n",
    "        if k == 1:\n",
    "            return trans_mat_sage()\n",
    "\n",
    "        # If k > 1\n",
    "        return self.Dinv @ self.T @ (np.linalg.matrix_power(self.Pnb, k - 1)) @ self.S\n",
    "\n",
    "    def nb_vertex_kemeny_mfpt(self, pinwheel=False):\n",
    "        \"\"\"\n",
    "        Use these hitting times to get Kemeny's constant\n",
    "        \"\"\"\n",
    "        volG = 2 * len(self.G.edges())\n",
    "        if pinwheel:\n",
    "            steady = np.array([d / volG for d in sorted(self.G.degree(), reverse=True)])\n",
    "        else:\n",
    "            steady = np.array([d / volG for d in self.G.degree()])\n",
    "        return steady @ self.Mv_nb @ steady\n",
    "    \n",
    "    def kemeny_vertex(self):\n",
    "        \"\"\"SRW Vertex Kemeny\"\"\"\n",
    "        eigvals = np.array(sorted(np.linalg.eigvals(self.P))[:-1])  # eigenvalues of P transition matrix without \\lambda_max = 1\n",
    "        return np.sum(1/(1-eigvals))\n",
    "        \n",
    "    def kemeney_edge(self):\n",
    "        \"\"\"SRW Edge Space Kemeny Theorem 2.9\"\"\"\n",
    "        return self.K_vertex + 2*self.m - self.n\n",
    "    \n",
    "    def kemeney_nb_edge(self):\n",
    "        \"\"\"nb kemeny's for a graph in the edge space\"\"\"\n",
    "        return np.trace(self.Znb_e) - 1\n",
    "    \n",
    "    def De_mat(self):\n",
    "        De = np.zeros((2*self.m, 2*self.m))\n",
    "        deg = self.G.degree()\n",
    "        for idx, tup in enumerate(self.G.edges()):\n",
    "            i, j, _ = tup\n",
    "            De[idx, idx] = deg[j]\n",
    "            De[self.m + idx, self.m + idx] = deg[i]\n",
    "        return De\n",
    "    \n",
    "    def nb_vertex_kemeny_trace(self):\n",
    "        return self.Knb_e - np.trace(self.De_inv @ self.Znb_e) + self.n * (1 - 1/(2*self.m)) - 2*self.m + self.n + np.trace(self.De_inv @ self.tau @ self.Znb_e)\n",
    "\n",
    "    def nb_fund_mat(self, i):\n",
    "        \"\"\"compute (I-Q)^-1 where Q = P[i,i], the transition probability matrix\n",
    "        with deleted cols/rows i\"\"\"\n",
    "        G = self.G\n",
    "        P = self.nb_trans_mat()\n",
    "        P = np.delete(P, i, 0)\n",
    "        P = np.delete(P, i, 1)\n",
    "        return np.linalg.inv(np.identity(len(P)) - P)\n",
    "\n",
    "\n",
    "    def nb_mfpt_mat(self):\n",
    "        \"\"\"Mean First Passing Time (Hitting Time) matrix in the edge space for\n",
    "        a Non-Backtracking Random Walk.\"\"\"\n",
    "        G = self.G\n",
    "        P = np.array(self.nb_trans_mat())\n",
    "        volG = len(P)\n",
    "\n",
    "        M = np.zeros((volG, volG))\n",
    "        for i in range(volG):\n",
    "            #get N for all directed edges\n",
    "            Ni = self.nb_fund_mat(i)\n",
    "            for j in range(len(Ni)):\n",
    "            #get all row sums, careful to put in correct place\n",
    "                if j < i:\n",
    "                    #if j index before i, no problem\n",
    "                    M[j,i] = np.sum(Ni[j,:])\n",
    "                if j >= i:\n",
    "                    #takes care of reindexing\n",
    "                    M[j+1,i] = np.sum(Ni[j,:])\n",
    "\n",
    "        return M\n",
    "\n",
    "    def fund_mat(self):\n",
    "        return np.linalg.inv(np.eye(self.n) - self.P + self.Wv)\n",
    "\n",
    "    def mfpt_mat(self, size, Z, pi):\n",
    "        M = np.zeros((size, size))\n",
    "        for i in range(size):\n",
    "            for j in range(size):\n",
    "                if i != j:\n",
    "                    M[i, j] = (Z[j, j] - Z[i, j]) / pi[j]\n",
    "        \n",
    "        return M\n",
    "    \n",
    "\n",
    "    def italian_hitting_times_mat(self):\n",
    "        diag_alpha = 1/(2*self.m) * np.eye(2*self.m) + 1/(2*self.m) * np.diag(np.diag(self.Pnb @ self.Mev @ self.T))\n",
    "        beta = np.diag(self.Dinv @ self.T @ self.Mnb_e @ diag_alpha @ self.T.T)\n",
    "        Mv_nb = self.Dinv @ self.T @ self.Mnb_e @ diag_alpha @ self.T.T - np.outer(np.ones(len(beta)), beta)\n",
    "        return Mv_nb\n",
    "    \n",
    "    def Re_mat(self):\n",
    "        return 2*self.m * np.eye(2*self.m)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "518d3dee",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"Code to generate various graph families. From Adam Knudson\"\"\"\n",
    "\n",
    "def cycle_barbell(k, a, b):\n",
    "    \"\"\"\n",
    "    path on k verties, the end two are shared with a cycle of size a and a cycle of size b\n",
    "    so |V(G)| = a + b + c - 2\n",
    "    \"\"\"\n",
    "    CB = Graph(k + a + b - 2)\n",
    "    \n",
    "    # make the a-cycle\n",
    "    for i in range(a - 1):\n",
    "        CB.add_edge(i, i+1)\n",
    "    CB.add_edge(0, a-1)\n",
    "\n",
    "    for i in range(k-1):\n",
    "        CB.add_edge(a-1+i, a+i)\n",
    "    \n",
    "    for i in range(b-1):\n",
    "        CB.add_edge(a+k-2+i, a+k-1+i)\n",
    "    CB.add_edge(a+k-2, a+b+k-3)\n",
    "\n",
    "\n",
    "    return CB\n",
    "\n",
    "\n",
    "def necklace(k):\n",
    "    \"\"\"\n",
    "    |V| = n = 4k + 2\n",
    "    k is number of \"beads\" including the end ones which are a little different\n",
    "    (so only works if k\\geq2)\n",
    "    \"\"\"\n",
    "    n = 4*k + 2\n",
    "    G = Graph(n)\n",
    "    #first bead\n",
    "    G.add_edges([(0,1), (0, 2), (0,3), (1,2), (1, 3), (2,4), (3,4)])\n",
    "    #last bead\n",
    "    G.add_edges([(n-1, n-2), (n-1, n-3), (n-1, n-4), (n-2, n-3), (n-2, n-4), (n-3, n-5), (n-4, n-5)])\n",
    "    #All the middle ones\n",
    "    for i in range(1, k-1):\n",
    "        G.add_edge(4*i, 4*i + 1)\n",
    "        G.add_edges([(4*i+1, 4*i + 2), (4*i+1, 4*i+3), (4*i+2, 4*i+3), (4*i+2, 4*(i+1)), (4*i+3, 4*(i+1))])\n",
    "        \n",
    "    #Final edge\n",
    "    G.add_edge(n - 6, n-5)\n",
    "    return G\n",
    "\n",
    "def pinwheel(cycle_sizes: list[int]):\n",
    "    \"\"\"Accepts a list of integers called cycle_sizes, which will determine the number \n",
    "    of cycles and also their length.\"\"\"\n",
    "    # Create an empty graph\n",
    "    G = Graph()\n",
    "    \n",
    "    # Add the central vertex\n",
    "    central_vertex = 0\n",
    "    \n",
    "    # Track the next available vertex id\n",
    "    next_vertex_id = 1\n",
    "    \n",
    "    for size in cycle_sizes:\n",
    "        # Create a cycle graph with an additional vertex for the central vertex\n",
    "        C = Graph()\n",
    "        C.add_vertex(central_vertex)\n",
    "        \n",
    "        # Add the vertices for the cycle\n",
    "        cycle_vertices = [next_vertex_id + i for i in range(size - 1)]\n",
    "        C.add_vertices(cycle_vertices)\n",
    "        \n",
    "        # Add edges to form the cycle including the central vertex\n",
    "        for i in range(size - 1):\n",
    "            if i == 0:\n",
    "                # Connect the central vertex to the first vertex in the cycle\n",
    "                C.add_edge(central_vertex, cycle_vertices[i])\n",
    "            else:\n",
    "                # Connect the previous vertex to the current vertex\n",
    "                C.add_edge(cycle_vertices[i-1], cycle_vertices[i])\n",
    "        \n",
    "        # Complete the cycle\n",
    "        C.add_edge(cycle_vertices[-1], central_vertex)\n",
    "        \n",
    "        # Add the cycle graph to G\n",
    "        G.add_vertices(C.vertices())\n",
    "        G.add_edges(C.edges())\n",
    "        \n",
    "        # Update the next available vertex id\n",
    "        next_vertex_id += size - 1\n",
    "    \n",
    "    return G\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "00b7717b",
   "metadata": {},
   "outputs": [],
   "source": [
    "def N(A):\n",
    "    two_m, _ = A.shape\n",
    "    return two_m * np.max(np.abs(A))\n",
    "\n",
    "def Bound(G):\n",
    "    \"\"\"accepts NBRW  object\"\"\"\n",
    "    m = G.m\n",
    "    I = np.eye(2*m)\n",
    "    top = N(I) - (N(I) - 1) * N(G.Pnb - G.Wnb)\n",
    "    bot = 1 - N(G.Pnb - G.Wnb)\n",
    "    return top / bot"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "58f160cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random as r\n",
    "def order_of_magnitude_on_cycle_barbell_graphs():\n",
    "    \"\"\"We want to establish/ conjecture that Knb_v is O(n^2), whereas Kv is O(n^3). The latter is known\"\"\"\n",
    "\n",
    "    plt.figure(figsize=(10, 10))\n",
    "    \n",
    "    v, nb_v, nb_v2, n = [], [], [], []\n",
    "    options = list(range(3, 41))\n",
    "    for _ in range(50):\n",
    "        i, j, k = r.sample(options, 3)\n",
    "        n.append(i + j + k)\n",
    "        g = cycle_barbell(i, j, k)\n",
    "        g = NBRW(g)\n",
    "        v.append(g.K_vertex)\n",
    "        nb_v.append(g.Knb_v2)\n",
    "        nb_v2.append(g.Knb_v)\n",
    "    \n",
    "    # plt.plot(dom, v, label=r\"$K^v$\")\n",
    "    plt.scatter(n, nb_v, c='skyblue', label=r\"$K_{nb}^v$\")\n",
    "    plt.scatter(n, nb_v2, c='k', label=r\"$K_{nb}^v$, $\\pi^TM\\pi$\")\n",
    "    plt.legend()\n",
    "    plt.title(\"Random Cycle Barbells\")\n",
    "    plt.xlabel(\"n\")\n",
    "    plt.ylabel(\"Kemeny's Constant\")\n",
    "    plt.show()\n",
    "    \n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0970cc65",
   "metadata": {},
   "outputs": [],
   "source": [
    "import random as r\n",
    "def order_of_magnitude_on_barbell_graphs():\n",
    "    \"\"\"We want to establish/ conjecture that Knb_v is O(n^2), whereas Kv is O(n^3). The latter is known\"\"\"\n",
    "\n",
    "    plt.figure(figsize=(10, 10))\n",
    "    \n",
    "    v, nb_v, nb_v2, n = [], [], [], []\n",
    "    options = list(range(3, 21))\n",
    "    for _ in range(5):\n",
    "        i, j = r.sample(options, 2)\n",
    "        n.append(i + j)\n",
    "        g = graphs.BarbellGraph(i, j)\n",
    "        g = NBRW(g)\n",
    "        v.append(g.K_vertex)\n",
    "        nb_v.append(g.Knb_v2)\n",
    "        nb_v2.append(g.Knb_v)\n",
    "    \n",
    "    # plt.plot(dom, v, label=r\"$K^v$\")\n",
    "    plt.scatter(n, nb_v, c='skyblue', label=r\"$K_{nb}^v$\")\n",
    "    plt.scatter(n, nb_v2, c='k', label=r\"$K_{nb}^v$, $\\pi^TM\\pi$\")\n",
    "    plt.legend()\n",
    "    plt.title(\"Random Barbells\")\n",
    "    plt.xlabel(\"n\")\n",
    "    plt.ylabel(\"Kemeny's Constant\")\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "86b00508",
   "metadata": {},
   "outputs": [],
   "source": [
    "def order_of_magnitude_on_barbell_graphs_deterministic():\n",
    "\n",
    "    plt.figure(figsize=(10, 10))\n",
    "\n",
    "    nb_v, nb_v2 = [], []\n",
    "    ns = [5, 8, 10, 12, 15, 18, 20]\n",
    "    for n in ns:\n",
    "        g = graphs.BarbellGraph(n, 1)\n",
    "        g = NBRW(g)\n",
    "        nb_v.append(g.Knb_v2)\n",
    "        nb_v2.append(g.Knb_v)\n",
    "\n",
    "    plt.scatter(ns, nb_v, c='skyblue', label=r\"$K_{nb}^v$\")\n",
    "    plt.scatter(ns, nb_v2, c='k', label=r\"$K_{nb}^v$, $\\pi^TM\\pi$\")\n",
    "    plt.legend()\n",
    "    plt.title(\"Random Barbells\")\n",
    "    plt.xlabel(\"n\")\n",
    "    plt.ylabel(\"Kemeny's Constant\")\n",
    "    plt.show()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b99e930",
   "metadata": {},
   "outputs": [],
   "source": [
    "def trace_upper_bound(G):\n",
    "    tau = G.tau\n",
    "    m = G.m\n",
    "    Z = G.Znb_e\n",
    "    Z_ = 1/2 * (Z + Z.T)\n",
    "    t_max = max(np.linalg.eigvals(tau))\n",
    "    z__min = min(np.linalg.eigvals(Z_))\n",
    "    return t_max * np.trace(Z) - z__min * (2*m* t_max - np.trace(tau))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3538d5c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy import linalg as la\n",
    "\n",
    "def Drazin(A, tol=1e-4):\n",
    "    \"\"\"Accepts a numpy nd-array and computes the Drazin inverse\n",
    "    \n",
    "    Parameters:\n",
    "        A ((n,n) ndarray): An nxn matrix.\n",
    "        tol (float): A float close to zero to classify 0 eigenvalues\n",
    "\n",
    "    Returns:\n",
    "        AD ((n,n) ndarray): The nxn Drazin Inverse of A\n",
    "    \"\"\"\n",
    "    n, n = A.shape\n",
    "    # This is some algorithm to compute the Drazin Inverse I found in an archived ACME Lab\n",
    "    T1, Q1, k1 = la.schur(A, sort=lambda x: abs(x) > tol)\n",
    "    T2, Q2, k2 = la.schur(A, sort=lambda x: abs(x) <= tol)\n",
    "    U = np.hstack((Q1[:, :k1], Q2[:, :n-k1]))\n",
    "    U_inv = la.inv(U)\n",
    "    V = U_inv @ A @ U\n",
    "    Z = np.zeros((n, n))\n",
    "    if k1 != 0:\n",
    "        Z[:k1, :k1] = la.inv(V[:k1, :k1])\n",
    "    return U @ Z @ U_inv\n",
    "\n",
    "\n",
    "def verify_if_drazin(A, k, AD):\n",
    "    \"\"\"Verify that a matrix AD is the Drazin inverse of A\n",
    "    \n",
    "    Parameters:\n",
    "        A ((n,n) ndarray): An nxn matrix.\n",
    "        AD ((n,n) ndarray): An nxn candidate for Drazin inverse of A\n",
    "        k (int): the index of A\n",
    "\n",
    "    Returns:\n",
    "        (bool) True of AD is the Drazin inverse of A, False otherwise\n",
    "    \"\"\"\n",
    "    is_drazin = True\n",
    "    # Test all cases that uniquely determine the drazin inverse of A\n",
    "    if not np.allclose(A@AD, AD@A, rtol=1e-3):  \n",
    "        return False, 1\n",
    "    B = np.linalg.matrix_power(A, k)  # only compute this once, A^k\n",
    "    if not np.allclose(B@A@AD, B, rtol=1e-3):\n",
    "        return False, 2\n",
    "    if not np.allclose(AD @ A @ AD, AD, rtol=1e-3):\n",
    "        return False, 3\n",
    "    \n",
    "    return is_drazin  # otherwise everything passed, so return true\n",
    "\n",
    "def index(A, tol=1e-5):\n",
    "    \"\"\"Compute the index of the matrix A \n",
    "    \n",
    "    Parameters:\n",
    "        A ((n,n) ndarray): An nxn matrix\n",
    "\n",
    "    Returns:\n",
    "        k (int): The index of A\n",
    "    \"\"\"\n",
    "\n",
    "    # test for non-singularity\n",
    "    if not np.allclose(la.det(A), 0):\n",
    "        return 0\n",
    "\n",
    "    n = len(A)\n",
    "    k = 1\n",
    "    Ak = A.copy()\n",
    "    while k <= n:\n",
    "        r1 = np.linalg.matrix_rank(Ak)\n",
    "        r2 = np.linalg.matrix_rank(np.dot(A, Ak))\n",
    "        if r1 == r2:  # equivalent to checking dimension of nullspaces\n",
    "            return k\n",
    "        Ak = np.dot(A, Ak)\n",
    "        k += 1\n",
    "    \n",
    "    return k\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0133aa5d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from matplotlib import pyplot as plt\n",
    "def plot_knbv_kv(n: int):\n",
    "    \"\"\"Code that plots both Knb^v(G) and K^v(G) for comparison. \n",
    "    Considers all complete graphs of minimum degree 2.\n",
    "    n = 8 take several minutes to run.\n",
    "\n",
    "    parameters:\n",
    "        k (int): max number of vertices to run on. \n",
    "    \"\"\"\n",
    "    for k in range(3, n+1):\n",
    "        for g in graphs.nauty_geng(f\"{k} -c -d2\"):\n",
    "            pass\n",
    "\n",
    "    ## Come back to. I'm not really sure the best way to plot this. Should vertices be on the x-axis? "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cc6164d8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def _get_Pnb_k(G, k):\n",
    "    return G.Dinv @ np.array(G.T) @ np.linalg.matrix_power(G.Pnb, k-1) @ np.array(G.S)\n",
    "\n",
    "\n",
    "def estimate_mu(G, max_iters=1000, tol=1e-5):\n",
    "    \"\"\"estimate the eigenvalues of Pnb^v\n",
    "    compute the lim_{k\\to\\infty} (mu_i^{(k)})^{1/k} for each i.\n",
    "    parameters:\n",
    "        G (NBRW) : the graph G as the NBRW class object\n",
    "    \"\"\"\n",
    "    Pnb_1 = _get_Pnb_k(G, 1)\n",
    "    mu = np.flip(np.sort(np.linalg.eigvals(Pnb_1)))\n",
    "    for k in range(2, max_iters+1):\n",
    "        Pnb_k = _get_Pnb_k(G, k)\n",
    "        mu_k = np.abs(np.flip(np.sort(np.linalg.eigvals(Pnb_k))))\n",
    "        # print(mu_k)\n",
    "        mu_k[np.abs(mu_k) < 1e-5] = 0.\n",
    "        # print(mu_k)\n",
    "        mu_k = mu_k ** (1/k)\n",
    "        # print(mu_k)\n",
    "\n",
    "        print()\n",
    "        print(mu_k)\n",
    "        print(mu)\n",
    "        print(mu_k - mu)\n",
    "        print(np.all(mu_k - mu < tol))\n",
    "        if np.all(mu_k - mu < tol):\n",
    "            print(\"Converged?\", k)\n",
    "            return mu_k\n",
    "        else:\n",
    "            mu = mu_k\n",
    "    \n",
    "    print(mu, mu_k, sep=\"\\n\")\n",
    "    print(mu_k - mu)\n",
    "    print(mu_k - mu < tol)\n",
    "    print(\"Probably didn't converge\")\n",
    "    return mu, mu_k\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a19e0ed9",
   "metadata": {},
   "outputs": [],
   "source": [
    "def is_diag_dom(A):\n",
    "    \"\"\"return True if A is diagonally dominant, False otherwise\n",
    "    Parameters:\n",
    "        A (ndarray) : matrix in question\n",
    "    Returns:\n",
    "        is_dd (bool) : True if dd False else\"\"\"\n",
    "    diags = np.diag(A)\n",
    "    row_sums = np.sum(np.abs(A), axis=1)\n",
    "    deleted_rs = row_sums - diags\n",
    "    if np.all(deleted_rs <= diags):\n",
    "        return True\n",
    "    return False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "54187e37",
   "metadata": {},
   "outputs": [],
   "source": [
    "import re\n",
    "\n",
    "class cb_expr_eval():\n",
    "    def __init__(self, expr: str) -> None:\n",
    "        # Preprocess the Mathematica expression to convert it into a Python-compatible format\n",
    "        expr = expr.replace(\"^\", \"**\")  # Replace Mathematica exponentiation with Python's exponentiation\n",
    "        expr = self.add_multiplication(expr)  # Add explicit multiplication\n",
    "        self.expr = expr\n",
    "\n",
    "    def add_multiplication(self, expr: str) -> str:\n",
    "        # Add explicit multiplication where needed\n",
    "        # Insert * between number and variable or variable and variable (e.g., 2a -> 2*a, ab -> a*b)\n",
    "        expr = re.sub(r'(\\d)([a-zA-Z])', r'\\1*\\2', expr)\n",
    "        expr = re.sub(r'([a-zA-Z])(\\d)', r'\\1*\\2', expr)\n",
    "        expr = re.sub(r'([a-zA-Z])([a-zA-Z])', r'\\1*\\2', expr)\n",
    "        return expr\n",
    "\n",
    "    def eval(self, a: int, k: int, b: int) -> float:\n",
    "        # Convert the input values to strings\n",
    "        a_str, k_str, b_str = str(a), str(k), str(b)\n",
    "        # Replace variables in the expression with their respective values\n",
    "        new_expr = self.expr.replace(\"a\", a_str).replace(\"k\", k_str).replace(\"b\", b_str)\n",
    "        # Evaluate the expression\n",
    "        return eval(new_expr)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2844b6b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def conjected_largest_graph_family(n: int, definition: str = \"one\") -> Graph:\n",
    "    \"\"\"On n vertices, finds the graph with the largest kemeny's constant\n",
    "        one -> pi.T @ M @ pi\n",
    "        two -> trace(Znb) - 1\n",
    "        three -> Knb_e - (2m - n)\"\"\"\n",
    "    max_kemeny = 0\n",
    "    for g in graphs.nauty_geng(f\"{n} -c -d2\"):\n",
    "        try:\n",
    "            g = NBRW(g)\n",
    "            if definition == \"one\":\n",
    "                val = g.Knb_v\n",
    "            elif definition == \"two\":\n",
    "                val = g.Knb_v2\n",
    "            else: \n",
    "                val = g.Knb_e - (2*g.m - g.n)\n",
    "            \n",
    "            if val > max_kemeny:\n",
    "                max_kemeny = val\n",
    "                max_graph = g\n",
    "        except np.linalg.LinAlgError:\n",
    "            pass\n",
    "    return max_graph"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "53860ee3",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
