A problem in indexing hub selection for a many-body state problem and erdos number
12:44 29 Jan 2026

I'm working on reproducing figure 8 of “Dynamical decoherence of a qubit coupled to a quantum dot or the SYK black hole,”.

The figure is:

enter image description here

In the paper "hub=0 corresponding to the many-body state where first 7 out of 16 orbitals are occupied." The Hamiltonian describing these states is:

enter image description here

I tried a python code to reproduce this figure. This code is very long so I will reproduce here only the part that is convenient for this question, but I can with no problem provide everything if needed. The code is:

    """
import numpy as np
import matplotlib.pyplot as plt
import time
import itertools
from scipy.special import comb
from collections import deque
from itertools import combinations
from matplotlib.ticker import LogLocator

class TBRIMHamiltonian:
    """
    Implementation of the spinless TBRIM Hamiltonian following the paper
    H = sum_k eps_k c_k^† c_k
        + (4 / sqrt(2 M^3)) sum_{i> k) & 1]

    def _generate_Vijkl(self):
        """
        Generate antisymmetrized two-body tensor
        INCLUDING diagonal interaction terms V_{ij,ij}
        """
        V = {}

        for i in range(self.M):
            for j in range(i + 1, self.M):

                # diagonal interaction (density-density)
                x_diag = np.random.normal(0.0, self.J)
                V[(i, j, i, j)] = x_diag
                V[(j, i, j, i)] = x_diag

                # off-diagonal scattering
                for k in range(self.M):
                    for l in range(k + 1, self.M):
                        if (i, j) == (k, l):
                            continue

                        x = np.random.normal(0.0, self.J)

                        V[(i, j, k, l)] =  x
                        V[(j, i, k, l)] = -x
                        V[(i, j, l, k)] = -x
                        V[(j, i, l, k)] =  x

        return V

    # Fermionic sign
    def _fermion_sign(self, state, annihilate, create):
        """
        Compute fermionic sign for applying:
        c_{create[0]}^† c_{create[1]}^† c_{annihilate[1]} c_{annihilate[0]}
        """
        sign = 1
        s = state

        # annihilation operators (right to left)
        for a in annihilate:
            sign *= (-1) ** (bin(s & ((1 << a) - 1)).count("1"))
            s &= ~(1 << a)

        # creation operators (left to right)
        for c in create:
            sign *= (-1) ** (bin(s & ((1 << c) - 1)).count("1"))
            s |= (1 << c)

        return sign

    # Matrix element
    def matrix_element(self, state_i, state_j):
        """Compute ⟨i|H|j⟩"""

        # Diagonal
        if state_i == state_j:
            occ = self._occupied_orbitals(state_i)

            # one-body term
            E = sum(self.eps_k[k] for k in occ)

            # diagonal interaction term
            for i, j in combinations(occ, 2):
                if (i, j, i, j) in self.Vijkl:
                    E += self.prefactor * self.Vijkl[(i, j, i, j)]

            return E

        # Off-diagonal: must differ by exactly 4 orbitals
        diff = state_i ^ state_j
        diff_orbs = [k for k in range(self.M) if (diff >> k) & 1]

        if len(diff_orbs) != 4:
            return 0.0

        occ_i = set(self._occupied_orbitals(state_i))
        occ_j = set(self._occupied_orbitals(state_j))

        annihilate = sorted([k for k in diff_orbs if k in occ_i])
        create = sorted([k for k in diff_orbs if k in occ_j])

        if len(annihilate) != 2 or len(create) != 2:
            return 0.0

        i, j = create
        k, l = annihilate

        if (i, j, k, l) not in self.Vijkl:
            return 0.0

        sign = self._fermion_sign(state_i, annihilate, create)
        return self.prefactor * sign * self.Vijkl[(i, j, k, l)]

    # Build full Hamiltonian
    def build_full_hamiltonian(self):
        """Construct the full many-body Hamiltonian matrix."""

        print("Building Hamiltonian matrix...")
        H = np.zeros((self.d, self.d))

        for a, state_a in enumerate(self.basis):
            H[a, a] = self.matrix_element(state_a, state_a)

            for b in range(a + 1, self.d):
                val = self.matrix_element(state_a, self.basis[b])
                if abs(val) > 1e-14:
                    H[a, b] = val
                    H[b, a] = val

        return H

class Network:

    def build_network(self, H, C):
        print(f"Building network with parameter C = {C}...")

        d = H.shape[0]
        adj = np.zeros((d, d), dtype=bool)

        diag = np.diag(H)

        # many-body level spacing estimate
        bandwidth = np.max(diag) - np.min(diag)
        Delta = bandwidth / d if bandwidth > 0 else 1.0 / d

        connections = 0
        for i in range(d):
            for j in range(i+1, d):
                denom = max(abs(diag[i] - diag[j]), Delta)
                if abs(H[i,j]) > C * denom:
                    adj[i,j] = True
                    adj[j,i] = True
                    connections += 1

        print(f"Created {connections} connections "
            f"(density : {100*connections/(d*(d-1)/2):.4f}%)")
        return adj, diag


    def compute_link_distribution(self, adj):
        """Compute the distribution of link numbers"""
        link_counts = np.sum(adj, axis=1)

        # Statistics
        mean_links = np.mean(link_counts)
        std_links = np.std(link_counts)

        # Histogram
        max_links = int(np.max(link_counts))
        bins = np.arange(0, max_links+2)
        hist, _ = np.histogram(link_counts, bins=bins)

        return link_counts, hist, bins, mean_links, std_links

    def compute_erdos_numbers(self, adj, hub_index):
        """
        Compute Erdös numbers using BFS
        (breadth-first search)
        """
        n = adj.shape[0]
        erdos = -np.ones(n, dtype=int) # -1 means unreachable
        erdos[hub_index] = 0

        queue = deque([hub_index])

        while queue:
            current = queue.popleft()
            current_dist = erdos[current]

            # Find neighbors
            neighbors = np.where(adj[current])[0]

            for neighbor in neighbors:
                if erdos[neighbor] == -1:
                    erdos[neighbor] = current_dist+1
                    queue.append(neighbor)
        
        # Statistics
        reachable_mask = erdos != -1
        reachable_erdos = erdos[reachable_mask]

        if len(reachable_erdos) > 1:
            mean_erdos = np.mean(reachable_erdos[reachable_erdos > 0])
            std_erdos = np.std(reachable_erdos[reachable_erdos > 0])
        else:
            mean_erdos = 0
            std_erdos = 0
        
        reachable_fraction = np.sum(reachable_mask) / n

        return erdos, reachable_fraction, mean_erdos, std_erdos

class PlotFigure:

    def plot_figure(self, results, param_sets, C_values):
        """
        Create all the plots
        """

        fig, axes = plt.subplots(2, 2, figsize=(14,10))

        # Plot for each C value
        for c_idx, C in enumerate(C_values):
            ax_erdos = axes[0, c_idx]
            ax_links = axes[1, c_idx]

            ax_erdos.set_title(f"Erdös Distribution (C={C})", fontsize=12)
            ax_erdos.set_xlabel("Erdős number $N_E$")
            ax_erdos.set_ylabel("Frequency $N_f(N_E)$")
        
            ax_links.set_title(f"Link Distribution (C={C})", fontsize=12)
            ax_links.set_xlabel("Link number $N_l$")
            ax_links.set_ylabel("Frequency $N_f(N_l)$")

            ax_erdos.set_yscale("log")
            ax_links.set_yscale("log")
            ax_links.set_xscale("log")
            #ax_erdos.set_ylim(bottom=1e-2)

            ax_erdos.yaxis.set_major_locator(LogLocator(base=10.0))
            ax_erdos.yaxis.set_minor_locator(LogLocator(base=10.0, subs="auto"))

            ax_links.yaxis.set_major_locator(LogLocator(base=10.0))
            ax_links.yaxis.set_minor_locator(LogLocator(base=10.0, subs="auto"))
            ax_links.xaxis.set_major_locator(LogLocator(base=10.0))
            ax_links.xaxis.set_minor_locator(LogLocator(base=10.0, subs="auto"))

            for params in param_sets:
                
                label = params['label']
                color = params['color']
                data = results[label][C]

                # Plot Erdös distribution
                erdos_counts = np.array(data["all_erdos"])
                unique_erdos, counts = np.unique(erdos_counts, return_counts=True)

                # normalize to probability
                counts = counts / n_realizations
                
                # Remove zero counts (log-scale safety)
                mask = counts > 0
                unique_erdos = unique_erdos[mask]
                counts = counts[mask]

                if len(unique_erdos) > 0:
                    ax_erdos.plot(
                        unique_erdos,
                        counts,
                        'o-',
                        color=color,
                        #label=f"{label}\nμ={data['mean_erdos']:.2f}±{data['std_erdos']:.2f}"
                        label=f"{label}\nV={params["V"]:.4f}, J={params["J"]:.4f}"

                    )

                # Plot links distribution
                link_counts = np.array(data["all_links"])

                # Normalize by maximum possible links
                #link_density = link_counts / (len(link_counts) - 1)

                # Optional: add tiny jitter to avoid single-point collapse
                #jitter = 0.01
                jitter = 0.5
                #link_density_jittered = link_density + np.random.uniform(-jitter, jitter, size=link_density.shape)
                link_counts_jittered = link_counts + np.random.uniform(-jitter, jitter, size=link_counts.shape)

                #unique_links, link_hist = np.unique(np.round(link_counts_jittered, 2), return_counts=True)

                max_links = int(np.max(link_counts))
                bins = np.arange(0, max_links+2)
                hist, _ = np.histogram(np.round(link_counts_jittered), bins=bins)

                unique_links = bins[:-1]
                #link_hist = hist
                link_hist = hist.astype(float)
                link_hist = link_hist / n_realizations
                
                if len(unique_links) > 0:
                    ax_links.plot(
                        unique_links,
                        link_hist,
                        's--',
                        color=color,
                        #label=f'{label}\nμ={data["mean_links"]:.2f}±{data["std_links"]:.2f}'
                        label=f"{label}\nV={params["V"]:.4f}, J={params["J"]:.4f}"
                    )

                """
                # Plot links distribution
                link_counts  =data['link_counts']
                unique_links, link_hist = np.unique(link_counts, return_counts=True)

                if len(unique_links) > 0:
                    ax_links.plot(unique_links, link_hist, 's--', color=color,
                                    label=f'{label}\nμ={data["mean_links"]:.2f}±{data["std_links"]:.2f}')
                """
                
            ax_erdos.legend(fontsize=8, loc='best')
            ax_links.legend(fontsize=8, loc='best')
            ax_erdos.grid(True, alpha=0.3)
            ax_links.grid(True, alpha=0.3)


        ymin, ymax = ax_erdos.get_ylim()
        ax_erdos.set_ylim(1e-2, ymax)

        ymin, ymax = ax_links.get_ylim()
        ax_links.set_ylim(1e-2, ymax)

        plt.suptitle(f"TBRIM Network Analysis, M={M} and L={L} for {n_realizations} realizations", fontsize=14)
        plt.tight_layout()
        plt.show()

        # Small-world properties
        print("\n" + "="*60)
        print("SMALL-WORLD ANALYSIS")
        print("="*60)

        for params in param_sets:

            label = params['label']
            print(f"\n{label}:")

            for C in C_values:
                data = results[label][C]
                adj = data['adj']
                n = adj.shape[0]

                # Average Path length
                sampled_paths = []
                for i in range(min(20, n)):
                    distances = self.bfs_from_node(adj, i)
                    valid_dist = distances[distances != -1]
                    if len(valid_dist) > 1:
                        sampled_paths.append(np.mean(valid_dist[valid_dist > 0]))
                
                avg_L = np.mean(sampled_paths) if sampled_paths else float('inf')

                # Connection probability
                p = np.mean(adj)

                # Random graph comparison
                if p > 0:
                    L_random = np.log(n) / np.log(p*n)
                    small_world_ratio = avg_L / L_random if L_random > 0 else float('inf')

                    print(f"  C={C}: L={avg_L:.2f}, L_random={L_random:.2f}, ratio={small_world_ratio:.2f}")

                    if 0.8 < small_world_ratio < 1.2 and data['mean_erdos'] < 4:
                        print(f"    ✓ Small-world property detected!")
                else:
                    print(f"   C={C}: No connections")
    
    def bfs_from_node(self, adj, start):
        """BFS to compute distances from start node"""

        n = adj.shape[0]
        distances = -np.ones(n, dtype=int)
        distances[start] = 0

        queue = deque([start])

        while queue:
            current = queue.popleft()
            current_dist = distances[current]

            neighbors = np.where(adj[current])[0]

            for neighbor in neighbors:
                if distances[neighbor] == -1:
                    distances[neighbor] = current_dist+1
                    queue.append(neighbor)
        
        return distances

class RunModelAndAnalysis:
    
    def run_model(self, M, L, C_values, n_realizations):
        """Run the model :
            Build the Hamiltonian
        """

        d = int(comb(M, L, exact=True))
        print(f"System: M={M}, L={L}, d={d}")

        # Define parameter sets
        param_sets = [
            {"V": 0.0, "J": 1.0, "label": "SYK", "color": "red"},
            {"V": np.sqrt(14.0), "J": 0.025, "label": "Weak", "color": "green"},
            {"V": np.sqrt(14.0), "J": 0.25, "label": "Moderate", "color": "blue"},
            {"V": np.sqrt(14.0), "J": 1.0, "label": "Strong", "color": "purple"}
        ]

        results = {}

        for params in param_sets:
            print(f"\n{'='*40}")
            print(f"Analyzing {params['label']}: V={params['V']}, J={params['J']}")
            print(f"\n{'='*40}")

            label = params["label"]

            results[label] = {
                C: {
                    "all_links": [],
                    "all_erdos": [],
                    "reach_frac": []
                } for C in C_values
            }

            for r in range(n_realizations):
                print(f"  Realization {r+1}/{n_realizations}")

                tbrim = TBRIMHamiltonian(
                    M, L, params['V'], params['J'],
                    seed=None  # important: different disorder each time
                )
                H = tbrim.build_full_hamiltonian()

                for C in C_values:
                    network = Network()
                    adj, diag = network.build_network(H, C)

                    # link counts
                    link_counts, *_ = network.compute_link_distribution(adj)

                    # hub selection
                    hub_index = 0

                    hub_state = tbrim.basis[hub_index]
                    occupied_orbitals = tbrim._occupied_orbitals(hub_state)
                    print(f"Hub state: index={hub_index}, occupied orbitals={occupied_orbitals}")

                    erdos, reach_frac, *_ = network.compute_erdos_numbers(adj, hub_index)

                    results[params['label']][C]["all_links"].extend(link_counts)
                    #results[params['label']][C]["all_erdos"].extend(erdos[erdos >= 0])
                    results[params['label']][C]["all_erdos"].extend(erdos)
                    results[params['label']][C]["reach_frac"].append(reach_frac)

        # Plot results
        figure = PlotFigure()
        figure.plot_figure(results, param_sets, C_values)
    
        return results

    def analyze_connectivity(self, M, L, C_values):
        """More detailed analysis of network conectivity"""

        d = int(comb(M, L, exact=True))

        print(f"\nDetailed analysis for M={M}, L={L}, d={d}")

        # Create Hamiltionian for two extreme cases
        print("\n1. SYK-like (V=0, J=1):")
        tbrim_syk = TBRIMHamiltonian(M, L, V=0.0, J=1.0)
        H_syk = tbrim_syk.build_full_hamiltonian()

        print("\n2. Weak interaction (V=1, J=0.05):")
        tbrim_weak = TBRIMHamiltonian(M, L, V=np.sqrt(14), J=0.025)
        H_weak = tbrim_weak.build_full_hamiltonian()

        # Analyze conectivity
        for C in C_values:

            print(f"\n{'='*50}")
            print(f"Network Analysis with C={C}")
            print(f"{'='*50}")

            for label, tbrim, H in [("SYK", tbrim_syk, H_syk), ("Weak", tbrim_weak, H_weak)]:

                print(f"\n{label}:")

                network = Network()
                adj, diag = network.build_network(H, C=C)

                # Count connections
                total_possible = d * (d - 1) // 2
                actual_connections = np.sum(adj) // 2
            
                print(f"  Total possible connections: {total_possible}")
                print(f"  Actual connections: {actual_connections}")
                print(f"  Connection density: {100*actual_connections/total_possible:.2f}%")
            
                # Analyze component structure
                components = self.find_connected_components(adj)
                print(f"  Number of connected components: {len(components)}")
            
                if len(components) > 0:
                    largest = max(components, key=len)
                    print(f"  Largest component size: {len(largest)}/{d} ({100*len(largest)/d:.1f}%)")
            
                # Compute average degree
                degrees = np.sum(adj, axis=1)
                print(f"  Average degree: {np.mean(degrees):.2f}")
                print(f"  Max degree: {np.max(degrees)}")
                print(f"  Min degree: {np.min(degrees)}")


    def find_connected_components(self, adj):
        """
        Find all connected components in the graph
        """
        
        n = adj.shape[0]
        visited = np.zeros(n, dtype=bool)
        components = []

        for i in range(n):
            if not visited[i]:
                # BFS to find component
                component = []
                queue = deque([i])
                visited[i] = True

                while queue:
                    node = queue.popleft()
                    component.append(node)

                    neighbors = np.where(adj[node])[0]
                    for neighbor in neighbors:
                        if not visited[neighbor]:
                            visited[neighbor] = True
                            queue.append(neighbor)
                
                components.append(component)
        
        return components


if __name__=="__main__":
    # Set random seed for reproducibility
    np.random.seed(42)

    M = 12 # orbitals
    L = 7  # particles

    C_values = [0.1, 1.0]

    n_realizations=2
    
    # Run
    model = RunModelAndAnalysis()
    results = model.run_model(M, L, C_values, n_realizations)

    # A detailed conectivity analysis
    model.analyze_connectivity(M, L, C_values)

The result I get:

enter image description here

which seems prretty much like the Fig9 from the paper (link above). In figure 9 they analyzed the hub=5720 ("corresponding to the many-body state where orbitals 3, 4, 8, 9, 12, 14, 15 out of 16 orbitals are occupied").

I concluded: or I am completely wrong or I am plotting hub=5720 while I was trying to plot hub=0, which means I am messing up with the selection process when I code:

                    # hub selection
                    hub_index = 0

                    hub_state = tbrim.basis[hub_index]
                    occupied_orbitals = tbrim._occupied_orbitals(hub_state)
                    print(f"Hub state: index={hub_index}, occupied orbitals={occupied_orbitals}")

                    erdos, reach_frac, *_ = network.compute_erdos_numbers(adj, hub_index)

If this is the case, that is, I am selecting hub=5720 instead of hub=0, then its a minor problem but I rewrote chunks of this code and the results were worse and worse. This is the best code I have but is not exactly doing what I was trying to do. This is my first time working with something like that, and I am writing this code for several days now.

Any help would be highly appreciated.

python python-3.x physics quantum-computing