Source code for qrisp.vqe.problems.heisenberg

"""
\********************************************************************************
* Copyright (c) 2023 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
"""

from qrisp.operators.qubit import X,Y,Z
from qrisp.core import x, h, cx, cp, gphase, rz
from qrisp.environments import conjugate
import networkx as nx


def greedy_edge_coloring(G, E=None):
    """
    This methods computes an edge coloring of a given graph.

    Parameters
    ----------
    G : nx.Graph
        The graph defining the lattice.
    E : list, optional
        A list of edges not to be considered for the first color.
    
    Returns
    -------
    edge_coloring : list
        An edge coloring of the graph.

    """

    edge_coloring = []

    G = G.copy()

    if E is not None and G.number_of_edges()>1:
        G.remove_edges_from(E)
        M = nx.maximal_matching(G)
        edge_coloring.append(M)
        G.remove_edges_from(M)
        G.add_edges_from(E)

    while G.number_of_edges()>0:
        M = nx.maximal_matching(G)
        edge_coloring.append(M)
        G.remove_edges_from(M)
    
    return edge_coloring

# sing gate corresponding to the singlet state $\ket{10}-\ket{01} of two qubits
def sing(a,b):
    x(a)
    h(a)
    x(b)
    cx(a,b)

# change of basis
def conjugator(a,b):
    cx(a,b)
    h(a)

#  heis gate corresponding to the unitary exp(-i*theta*(XX+YY+ZZ))
def heis(theta,a,b):
    with conjugate(conjugator)(a,b):
        cp(theta,a,b)
        gphase(-theta/2,a)

[docs] def create_heisenberg_hamiltonian(G, J, B): """ This method creates the Hamiltonian for the Heisenberg model. Parameters ---------- G : nx.Graph The graph defining the lattice. J : float The positive coupling constant. B : float The magnetic field strength. Returns ------- H : :ref:`QubitOperator` The quantum Hamiltonian. """ H = sum(J*X(i)*X(j)+Y(i)*Y(j)+Z(i)*Z(j) for (i,j) in G.edges()) + sum(B*Z(i) for i in G.nodes) return H
[docs] def create_heisenberg_ansatz(G, J, B, M, C, ansatz_type="per hamiltonian"): """ This method creates a function for applying one layer of the ansatz. Parameters ---------- G : nx.Graph The graph defining the lattice. J : float The positive coupling constant. B : float The magnetic field strength. M : list A list of edges corresponding to a maximal matching of ``G``. C : list An edge coloring of the graph ``G`` given by a list of lists of edges. ansatz_type : string, optional Specifies the Hamiltonian Variational Ansatz. Available are ``per hamiltonian``, ``per edge color``, ``per edge``. The default is ``per hamiltonian``. Returns ------- ansatz : function A function that can be applied to a :ref:`QuantumVariable` and a list of parameters. """ # per hamiltonian def ansatz(qv,theta): # apply H_0 for (i,j) in M: heis(theta[0],qv[i],qv[j]) # apply H rz(B*theta[1],qv) for edges in C: for (i,j) in edges: heis(J*theta[1],qv[i],qv[j]) #per edge color def ansatz_per_edge_color(qv,theta): # apply H_0 for (i,j) in M: heis(theta[0],qv[i],qv[j]) # apply H rz(B*theta[1],qv) count = 0 for edges in C: for (i,j) in edges: heis(J*theta[2+count],qv[i],qv[j]) count += 1 #per edge def ansatz_per_edge(qv,theta): # apply H_0 for (i,j) in M: heis(theta[0],qv[i],qv[j]) # apply H rz(B*theta[1],qv) count = 0 for edges in C: for (i,j) in edges: heis(J*theta[2+count],qv[i],qv[j]) count += 1 if ansatz_type=="per edge color": return ansatz_per_edge_color if ansatz_type=="per edge": return ansatz_per_edge return ansatz
[docs] def create_heisenberg_init_function(M): """ Creates the function that, when applied to a :ref:`QuantumVariable`, initializes a tensor product of singlet sates corresponding to a given matching. Parameters ---------- M : list A list of edges corresponding to a maximal matching of ``G``. Returns ------- init_function : function A function that can be applied to a :ref:`QuantumVariable`. """ def init_function(qv): # tensor product of singlet states for (i,j) in M: sing(qv[i],qv[j]) return init_function
[docs] def heisenberg_problem(G, J, B, ansatz_type="per hamiltonian"): r""" Creates a VQE problem instance for an isotropic Heisenberg model defined by a graph $G=(V,E)$, the coupling constant $J>0$ (antiferromagnetic), and the magnetic field strength $B$. The model Hamiltonian is given by: .. math:: H = J\sum\limits_{(i,j)\in E}(X_iX_j+Y_iY_j+Z_iZ_j)+B\sum\limits_{i\in V}Z_i Each Hamiltonian $H_{i,j}=X_iX_j+Y_iY_j+Z_iZ_j$ has three eigenvectors for eigenvalue $+1$ (triplet states): * $\ket{00}$ * $\ket{11}$ * $\frac{1}{\sqrt{2}}\left(\ket{10}+\ket{01}\right)$ and one eigenvector for eigenvalue $-3$ (singlet state): * $\frac{1}{\sqrt{2}}\left(\ket{10}-\ket{01}\right)$ For the problem specific VQE ansatz, we choose a Hamiltonian Variational Ansatz as proposed `here <https://arxiv.org/abs/2108.08086>`_. This ansatz is inspired by the adiabatic theorem of quantum mechanics: A system is prepared in the ground state of an initial Hamiltonian $H_0$ and then slowly evolved under a time-dependet Hamiltoniam $H(t)$. Here, we set .. math:: H(t) = \left(1-\frac{t}{T}\right)H_0 + \frac{t}{T}H where .. math:: H_0 = \sum\limits_{(i,j)\in M}(X_iX_j+Y_iY_j+Z_iZ_j) for a maximal matching $M\subset E$ of the graph $G$. For $J>0$ the ground state of the initial Hamiltonian $H_0$ is given by a tensor product of singlet states corresponding to the maximal matching $M$. The time evolution of $H(t)$ is approximately implemented by trotterization, i.e., alternatingly applying $e^{-iH_0\Delta t}$ and $e^{-iH\Delta t}$, and if necessary trotterizing $e^{-iH\Delta t}$. In the scope of VQE, the short evolution times $\Delta t$ are replaced by parameters $\theta_i$ which are then optimized. This yields the following unitary ansatz with $p$ layers: .. math:: U(\theta) = \prod_{l=1}^{p}e^{-i\theta_{l,0}H_0}e^{-i\theta_{l,1}H} The unitary $e^{-i\theta H}$ trotterized by: .. math:: U_H(\theta) = e^{-i\theta H_B}\prod\limits_{k=1}^{q}\prod_{(i,j)\in E_k}e^{-i\theta H_{ij}} where $E_1,\dotsc,E_q$ is an edge coloring of the graph $G$, and $H_B$ is the magnetic field Hamiltonian. Then all unitaries $e^{-i\theta H_{ij}}$ for $(i,j)\in E_k$ commute. For implementing such unitaries, note that each two-qubit `Heisenberg interaction unitary <https://arxiv.org/abs/2108.02175>`_ .. math:: \text{Heis}(\theta) \equiv e^{-i\theta/4}e^{-i\theta H_{ij}} = \begin{pmatrix} e^{-i\theta/2}&0&0&0\\ 0&\cos(\theta/2)&-i\sin(\theta/2)&0\\ 0&-i\sin(\theta/2)&\cos(\theta/2)&0\\ 0&0&0&e^{-i\theta/2} \end{pmatrix} becomes diagonal in Bell basis, i.e., $e^{-i\theta/2}\text{diag}(1,1,1,e^{i\theta})$. This ansatz can be further generalized by introducing parameters * per edge color (one parameter for each color) * per edge (one parameter for each edge) in the unitary $U_H(\theta)$. Parameters ---------- G : nx.Graph The graph defining the lattice. J : float The positive coupling constant. B : float fhe magnetic field strength. ansatz_type : string, optional Specifies the Hamiltonian Variational Ansatz. Available are ``per hamiltonian``, ``per edge color``, ``per edge``. The default is ``per hamiltonian``. Returns ------- VQEProblem VQE problem instance for a specific isotropic Heisenberg model. Examples -------- :: import networkx as nx import matplotlib.pyplot as plt # Create a graph G = nx.Graph() G.add_edges_from([(0,1),(1,2),(2,3),(0,3)]) # Draw the graph with labels pos = nx.spring_layout(G) nx.draw(G, pos, with_labels=True, node_size=700, node_color='lightblue') nx.draw_networkx_labels(G, pos) plt.show() .. figure:: /_static/heisenberg_lattice.png :scale: 80% :align: center :: from qrisp import QuantumVariable from qrisp.vqe.problems.heisenberg import * vqe = heisenberg_problem(G,1,1) vqe.set_callback() energy = vqe.run(QuantumVariable(G.number_of_nodes()),depth=2,max_iter=50) print(energy) # Yields -8.0 We visualize the optimization process: >>> vqe.visualize_energy(exact=True) .. figure:: /_static/heisenberg_energy.png :scale: 80% :align: center """ from qrisp.vqe import VQEProblem M = nx.maximal_matching(G) C = greedy_edge_coloring(G,M) num_params = 2 if ansatz_type=="per edge color": num_params = 2+len(C) if ansatz_type=="per edge": num_params = 2+G.number_of_edges() return VQEProblem(create_heisenberg_hamiltonian(G,J,B), create_heisenberg_ansatz(G,J,B,M,C, ansatz_type=ansatz_type), num_params=num_params, init_function=create_heisenberg_init_function(M))