import marimo

__generated_with = "0.11.5"
app = marimo.App(layout_file="layouts/rtn-nechita-fermi-days-2025.slides.json")


@app.cell
def _(mo):
    mo.md(
        r"""
        # Entanglement in random tensor networks

        **Ion Nechita** (CNRS, LPT Toulouse)

        Joint work with **Khurshed Fitter** (former intern at LPT, now EPFL), **Faedi Loulidi** (former PhD student at LPT, now OIST, Japan), and **Miao Hu** (PhD student, LPT)

        Talk mostly based on https://arxiv.org/abs/2407.02559 



        **Outline.** We explore the *entanglement properties* of *random tensor networks*. We introduce a model of random tensor networks and compute the entanglement entropy of a bipartition of the system. We will also visualize the *entanglement spectrum* as a function of the underlying graph structure of the network.
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ## Entanglement of quantum states

        Given a bipartite quanutm state $\ket \psi_{AB} \in \mathcal H_A \otimes  \mathcal H_B$, we define its [**entropy of entanglement**](https://en.wikipedia.org/wiki/Entropy_of_entanglement) as 

        $$ S_{\text{ent}}(\ket \psi) = -\sum_i \lambda_i \log \lambda_i,$$

        where $\lambda_i$ are the *Schmidt coefficients* of the state: 

        $$ \ket \psi = \sum_i \sqrt{\lambda_i} \ket{a_i} \otimes \ket{b_i}.$$

        For example, a *separable state* has zero entropy of entanglement 

        $$S_{\text{ent}}(\ket{00}) = 0$$

        while a maximally entangled state of two qubits has unit entropy of entanglement

        $$S_{\text{ent}}\left(\frac{\ket{00} + \ket{11}}{\sqrt 2}\right) = \log 2  =1.$$

        In general, for a state on two *qudits* $\ket \psi \in \mathbb C^d \otimes \mathbb C^d$, we have 

        $$0 \leq S_{\text{ent}}(\ket \psi) \leq \log d.$$
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ### Random states. Page formula

        We consider now random states $\ket \psi_{AB} \in \mathbb C^{d_A} \otimes \mathbb C^{d_B}$, obtained by randomly rotating a fixed state by a uniform *global* unitary rotation 

        $$\ket \psi = U \cdot \ket{\psi_0},$$
        where $U \in \mathcal U(d_Ad_B)$ is a [Haar-distributed](https://en.wikipedia.org/wiki/Haar_measure) **random unitary matrix**. Equivalently, $\ket \psi$ is chosen **uniformly at random from the unit sphere** of $\mathbb C^{d_A \cdot d_B}$.

        Page conjectured in 1993 (and it was later proved) that the *average* entropy of entanglement of a random state is given by (we assume wlog $d_A\leq d_B$)

        $$\mathbb E S_{\text{ent}}(\ket \psi) = \sum_{i=d_B+1}^{d_A d_B} \frac{1}{i} - \frac{d_A-1}{2d_B}.$$

        In the limit $d_A = d_B = d\to \infty$, we have
        $$\boxed{\mathbb E S_{\text{ent}}(\psi) = \log d - \frac 1 2 + o(1).}$$
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ### Entanglement spectrum

        One can also study the **entanglement spectrum** of a bipartite state $\ket \psi_{AB}$. The entanglement spectrum is defined as the set of Schmidt coefficients $\lambda_i$ or, equivalently, as the set of eigenvalues of the reduced density matrix $\rho_A = \text{Tr}_B \ket \psi_{AB} \bra \psi$.

        Note that the scalars $\lambda_i$ are non-negative 

        $$ \lambda_i \geq 0 \quad \forall i$$

        and normalized

        $$\sum_i \lambda_i = 1.$$

        In other words, they form a *probability distribution*. The entropy of entanglement of the state is then just the [Shannon entropy](https://en.wikipedia.org/wiki/Entropy_(information_theory)) of the vector $\lambda = (\lambda_i)_i$.
        """
    )
    return


@app.cell
def _(mo):
    d_slider_biparite = mo.ui.slider(start=2, stop=1000, step=1, value=10, label="Local Hilbert space dimension d:")
    d_slider_FC = mo.ui.slider(start=2, stop=1000, step=1, value=10, label="Local Hilbert space dimension d:")
    return d_slider_FC, d_slider_biparite


@app.cell
def _(create_plot_MP, d_slider_biparite, mo):
    # Create the plot using the current slider value
    # This plot variable implicitly depends on 'amplitude_slider'
    spectrum = create_plot_MP(d_slider_biparite.value)

    # Arrange the slider and the plot vertically and display them
    # This is the last expression in the cell
    mo.vstack([d_slider_biparite, spectrum.gca()])
    return (spectrum,)


@app.cell
def _(np, plt):
    # Define the Marchenko-Pastur PDF for c=1, sigma^2=1
    # lambda_minus = (1 - sqrt(c))^2 = (1-1)^2 = 0
    # lambda_plus = (1 + sqrt(c))^2 = (1+1)^2 = 4
    # Support is [0, 4]
    def marchenko_pastur_pdf(x):
        # Avoid division by zero and sqrt of negative numbers
        # Use np.clip to keep x within a safe range slightly inside (0, 4)
        x_safe = np.clip(x, 1e-9, 4.0 - 1e-9)
        pdf = np.sqrt(x_safe * (4.0 - x_safe)) / (2 * np.pi * x_safe)
        # Set density to 0 outside the support [0, 4]
        pdf[x < 0] = 0
        pdf[x > 4] = 0
        return pdf

    def create_plot_MP(d):
        n_samples = 1

        # Generate the complex Wishart matrix and compute eigenvalues
        eigenvalues = []
        for _ in range(n_samples):
            # Generate a d x d matrix with complex standard normal entries
            # E[|X_ij|^2] = E[(Re(X_ij))^2] + E[(Im(X_ij))^2] = 1/2 + 1/2 = 1
            X = (np.random.randn(d, d) + 1j * np.random.randn(d, d)) / np.sqrt(2)

            # Compute the Wishart matrix W = (1/d) * X^dagger * X
            W = (X.conj().T @ X) / d

            # Compute the eigenvalues (W is Hermitian, so eigenvalues are real)
            evals = np.linalg.eigvalsh(W)
            eigenvalues.extend(evals)

        eigenvalues = np.array(eigenvalues)



        # Generate x values for the theoretical curve
        x_theory = np.linspace(0, 4.1, 500) # Extend slightly beyond 4 for plotting
        y_theory = marchenko_pastur_pdf(x_theory)

        # Create the plot
        plt.style.use('default')
        plt.figure(figsize=(10, 6))

        # Plot the histogram of the eigenvalues
        # Use density=True to normalize the histogram
        count, bins, ignored = plt.hist(eigenvalues, bins=50, density=True, label='Entanglement spectrum', alpha=0.7)

        # Plot the theoretical Marchenko-Pastur density
        plt.plot(x_theory, y_theory, '-r', linewidth=2, label='Marchenko-Pastur PDF')

        # Add labels and title
        plt.xlabel('Eigenvalue')
        plt.ylabel('Density')
        plt.title(f'Entanglement spectrum of a random state (d={d}) vs Marchenko-Pastur Law')
        plt.legend()
        plt.grid(True)
        plt.ylim(bottom=0, top=3) # Ensure y-axis starts at 0
        plt.xlim(left=-0.1, right = max(4.1, np.max(eigenvalues)*1.05)) # Adjust x-axis limits

        return plt
    return create_plot_MP, marchenko_pastur_pdf


@app.cell
def _(mo):
    mo.md(
        r"""
        We observe that as the dimension $d$ grows to infinity, the probability distribution $\mu$ converges to a deterministic distribution, the [**Marchenko-Pastur distribution**](https://en.wikipedia.org/wiki/Marchenko%E2%80%93Pastur_distribution). This distribution is supported on the interval $[0, 4]$ and has a density given by

        $$\mathrm{d}MP(x) = \frac{\sqrt{x(4-x)}}{2\pi x} \mathbb{1}_{(0,4)}(x) \, \mathrm{d}x,$$

        For such a distribution of the entanglement spectrum, the entanglement entropy behaves asymptotically like $\log d$, a behavior commonly known as the **volume law**: the entropy scales like the size (volume) of the system.

        More precisely, we have 

        $$\boxed{S_{\text{ent}}(\ket \psi) =  \log d - c + o(1)}$$

        where $c$ is a constant correction given by the entropy of the limiting distribution of the entanglement spectrum

        $$c = \int_0^4 x \log x \,  \mathrm{d}MP(x) = \frac 1 2,$$

        which fits the value predicted by the Page formula.
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ### Beyond uniform random states

        Uniform random quantum states are unphysical due to volume-law entanglement (entropy scales with system size), unlike physical states (ground states of local Hamiltonians) which obey the [**area law**](https://en.wikipedia.org/wiki/Entropy_of_entanglement#Area_law_of_bipartite_entanglement_entropy) (entropy scales with boundary area) and occupy a small, structured corner of the Hilbert space.

        ![area law](public/area-law.png)
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ## Tensor networks

        Tensor networks, by their construction, efficiently parameterize states that naturally obey the area law. Their inherent structure limits the amount of entanglement a state can possess, making them a much more physically realistic model for describing the low-energy states of many-body quantum systems and capturing the essential physics without the exponential complexity of generic Hilbert space vectors.

        ![tensor-networks](public/tensor-networks.png)
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        A tensor network is a graphical representation of a quantum state, where the vertices (nodes) correspond to tensors and edges are of two types: 

        - *bulk edges* (or bonds) that connect tensors to each other and corespond to tensor contractions
        - *boundary edges* (or legs) that connect tensors to the physical degrees of freedom of the system. 

        ![fig1](public/fig1.png)

        It is common to partition the boundary edges in two parts $\textcolor{orange}{A} \sqcup \textcolor{green}{B}$ corresponding to a bipartition of the physical degrees of freedom. 

        The entanglement of the tensor network state shall be understood with respect to this bipartion.
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ### Cuts in tensor networks

        ![fig1-cut](public/fig1-cut.png)

        A **cut** in a tensor network (with a given partition of the boundary edges $A \sqcup B$) is a set of edges that, if removed, disconnect $A$ from $B$. 

        Any cut induces an *upper bound* on the rank of the induced linear map $A \to B$

        $$ \operatorname{rank}L_{A \to B} \leq d^{\text{ size of the cut}}$$

        In the example above, although $\dim \mathcal H_A = \dim \mathcal H_B = d^{5}$, the blue cut induced a bound of $d^4$ on the rank, and thus on the entanglement entropy

        $$ S_{\text{ent}}(\ket \psi_{AB}) \leq 4 \cdot \log d.$$
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ### Random tensor networks

        Start with **independent Gaussian tensors** $g_x$ at each verte of the network.

        ![fig1-tensors](public/fig1-tensors.png)

        Then, contract these tensors along the bulk edges $e \in E_b$: 

        $$    \ket{\psi_G}:=\Big\langle \bigotimes_{e\in E_b}\Omega_e \;  \Big | \; \bigotimes_{x\in V} g_x \Big\rangle \in  \big(\mathbb C^D\big)^{\otimes|E_{\partial}|}$$
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ## Main result

        **Theorem.** In the limit $d \to \infty$, the average entropy of entanglement of a random tensor network state $\ket{\psi_G}$ behvaes as
        $$\mathbb E S_{\text{ent}}(\ket{\psi_G}) = \operatorname{mincut}(G_{A|B}) \cdot \log d -\int t\,\log t\,\mathrm{d}\mu_{G_{A|B}}+o(1),$$
        where $\operatorname{mincut}(G_{A|B})$ is the **minimal cut** in the network $G_{A|B}$ and $\mu_{G_{A|B}}$ is a probability distribution that depends on the network. More precisely, this measure depends on the number of minimal cuts and on the inclusion structure of the minimal cuts. 

        </br>

        Note that in any network, $\operatorname{mincut}(G_{A|B}) = \operatorname{maxflow}(G_{A|B})$, the **maximum flow** that can be sent in the network from nodes in $A$ to nodes in $B$. By duality, the result above can be formulated in terms of maximal flows in the network.
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ### Series-parallel networks

        The measure $\mu_{G_{A|B}}$ can be *explicitly computed* in the case of **[series-parallel](https://en.wikipedia.org/wiki/Series%E2%80%93parallel_graph)** networks, using tools from [free probability theory](https://en.wikipedia.org/wiki/Free_probability). Series-parallel networks can be obtained by recursively by the two operations below. 

        ![series-parallel](public/series-parallel.png)

        The network discussed before is series-parallel, and we have

        $$\mu_{G_{A|B}} = \left\{\left[ \mathrm{MP}^{\boxtimes 3} \boxtimes (\mathrm{MP}^{\boxtimes 2} \times \mathrm{MP}) \right] \times \left[ (\mathrm{MP} \times \mathrm{MP}) \boxtimes \mathrm{MP} \right] \right\} \boxtimes \mathrm{MP}^{\boxtimes 2}.$$
        """
    )
    return


@app.cell
def _(mo):
    mo.md(
        r"""
        ### An example

        The completely random state that follows the Page law corresponds to the structureless network 
        ![N1](public/N1.png)
        Consider now the next simplest network 
        ![N1](public/N2.png)

        This network also corresponds to a random state $\ket \psi_{AB} \in \mathbb C^d \otimes \mathbb C^d$, with an internal structure: one bulk edge connects the two independent random tensors (here: matrices) $g_1$ and $g_2$. 

        Using our tools, we can show that the limiting distribution of the entanglement spectrum of $\ket \psi_{AB}$ is given by the **free multiplicative convolution** of two Marchenko-Pastur distributions, and that

        $$\boxed{S_{\text{ent}}(\ket \psi_{AB}) = 1 \cdot \log d - \frac 5 6 +o
        (1).}$$

        Hence, the correction to the volume law is *larger* than in the uniform case.
        """
    )
    return


@app.cell
def _(np, plt):
    # Define the Marchenko-Pastur PDF for c=1, sigma^2=1
    # lambda_minus = (1 - sqrt(c))^2 = (1-1)^2 = 0
    # lambda_plus = (1 + sqrt(c))^2 = (1+1)^2 = 4
    # Support is [0, 4]
    def FC_pdf(x):
        """Fuss-Catalan P_2(x) distribution density."""
        density = np.zeros_like(x, dtype=float)
        mask = (x > 1e-6) & (x < 27./4.)
        xm = x[mask]
        sqrt_term = np.sqrt(np.maximum(0., 81. - 12. * xm))
        term_brackets = np.maximum(0., 27. + 3. * sqrt_term)
        term_pow_1_3 = np.power(term_brackets, 1./3.)
        term_pow_2_3 = np.power(term_brackets, 2./3.)
        x_pow_1_3 = np.power(xm, 1./3.)
        x_pow_2_3 = np.power(xm, 2./3.)
        const = (np.sqrt(3.) * np.power(2., 1./3.)) / (12. * np.pi)
        num = (np.power(2., 1./3.) * term_pow_2_3 - 6. * x_pow_1_3)
        den = x_pow_2_3 * term_pow_1_3
        valid_den = np.abs(den) > 1e-15
        density_masked = np.zeros_like(xm)
        density_masked[valid_den] = const * num[valid_den] / den[valid_den]
        density[mask] = density_masked
        return np.maximum(0, np.nan_to_num(density))

    def create_plot_FC(d):
        n_samples = 1

        # Generate the complex Wishart matrix and compute eigenvalues
        eigenvalues = []
        for _ in range(n_samples):
            # Generate a d x d matrix with complex standard normal entries
            # E[|X_ij|^2] = E[(Re(X_ij))^2] + E[(Im(X_ij))^2] = 1/2 + 1/2 = 1
            X = (np.random.randn(d, d) + 1j * np.random.randn(d, d)) / np.sqrt(2)
            Y = (np.random.randn(d, d) + 1j * np.random.randn(d, d)) / np.sqrt(2)

            # Compute the Wishart matrix W = (1/d) * X^dagger * X
            W = (X @ Y @ Y.conj().T @ X.conj().T) / d**2

            # Compute the eigenvalues (W is Hermitian, so eigenvalues are real)
            evals = np.linalg.eigvalsh(W)
            eigenvalues.extend(evals)

        eigenvalues = np.array(eigenvalues)



        # Generate x values for the theoretical curve
        x_theory = np.linspace(0, 27/4.0+0.1, 500) # Extend slightly beyond 4 for plotting
        y_theory = FC_pdf(x_theory)

        # Create the plot
        plt.style.use('default')
        plt.figure(figsize=(10, 6))

        # Plot the histogram of the eigenvalues
        # Use density=True to normalize the histogram
        count, bins, ignored = plt.hist(eigenvalues, bins=50, density=True, label='Entanglement spectrum', alpha=0.7)

        # Plot the theoretical Marchenko-Pastur density
        plt.plot(x_theory, y_theory, '-r', linewidth=2, label='Fuss-Catalan(2) PDF')

        # Add labels and title
        plt.xlabel('Eigenvalue')
        plt.ylabel('Density')
        plt.title(f'Entanglement spectrum of a RTN [--1--2--] (d={d}) vs Fuss-Catalan(2) Law')
        plt.legend()
        plt.grid(True)
        plt.ylim(bottom=0, top=2) # Ensure y-axis starts at 0
        plt.xlim(left=-0.1, right = max(27/4.0+1, np.max(eigenvalues)*1.05)) # Adjust x-axis limits

        return plt
    return FC_pdf, create_plot_FC


@app.cell
def _(create_plot_FC, d_slider_FC, mo):
    # Create the plot using the current slider value
    # This plot variable implicitly depends on 'amplitude_slider'
    spectrumFC = create_plot_FC(d_slider_FC.value)

    # Arrange the slider and the plot vertically and display them
    # This is the last expression in the cell
    mo.vstack([d_slider_FC, spectrumFC.gca()])
    return (spectrumFC,)


@app.cell
def _(mo):
    mo.md(
        r"""
        ## Take home message

        Tthe average entropy of entanglement of a random tensor network state $\ket{\psi_G}$ behvaes as
        $$\mathbb E S_{\text{ent}}(\ket{\psi_G}) = \operatorname{mincut}(G_{A|B}) \cdot \log d -\int t\,\log t\,\mathrm{d}\mu_{G_{A|B}}+o(1),$$

        where $\operatorname{mincut}(G_{A|B})$ is the **minimal cut** in the network $G_{A|B}$ and $\mu_{G_{A|B}}$ is a probability distribution that depends on the network.

        Our **main contribution** is the explicit computation, in the *series-parallel case*, of the limiting measure $\mu_{G_{A|B}}$ which gives the constant correction to the area law.

        ![fig1-cut](public/fig1-cut.png)
        """
    )
    return


@app.cell(hide_code=True)
def _():
    import marimo as mo
    import numpy as np
    import matplotlib.pyplot as plt
    import math
    return math, mo, np, plt


@app.cell
def _():
    return


if __name__ == "__main__":
    app.run()
