CUDA-Q x Fermioniq#

Fermioniq’s emulator is also integrated with the NVIDIA CUDA-Q platform for quantum computing. Here we show you an example of how to run quantum circuits on the Fermioniq backend, using the CUDA-Q Python API.

  1. Install the CUDA-Q Python package

  2. Save your credentials in the environment variables FERMIONIQ_ACCESS_TOKEN_ID and FERMIONIQ_ACCESS_TOKEN_SECRET

  3. Use the cudaq.set_target() function to specify that you wish to use the Fermioniq backend

import cudaq
import numpy as np
cudaq.set_target('fermioniq')

It is also possible to specify the remote config, which project within your organization you are using, and emulation parameters like the bond dimension.

cudaq.set_target("fermioniq", **{
    "remote_config": "<my-remote-config-id>",
    "project_id": "<my-project-id>",
    "bond_dim": 200
})

We should now be ready to run CUDA-Q circuits with the Fermioniq backend! In this example we will simulate the time evolution of a lattice of spins, using the transverse field Ising model.

\(H = J \sum_{\langle i,j \rangle} Z_i Z_j + h \sum_i X_i \)

The first term describes the energy of the coupling between neighbouring spins, and the second one the energy contribution of a transverse magnetic field. A more detailed explanation of the underlying ideas behind this quantum circuit can be seen in our blog post. We start by defining some parameters of the simulation.

# Parameters for the Ising model and Trotterization
J = -1. # Coupling strength
hx = -2. # Transverse field strength
DT = 0.2 # Time step size
LX = 4 # Number of spins in x-direction
LY = 4 # Number of spins in y-direction
N_QUBITS = LX * LY # Total number of qubits
THETA_INIT = 0. # Angle defining the initial state of the qubits
N_TROTTER_STEPS = 20 # Number of Trotter steps
TROTTER_ORDER = 2 # Order of the Trotterization

The \(R_{ZZ}(\phi)\) operator is not natively supported in CUDA-Q so we need to register it ourselves. For our simulation we use \(\phi = 2 \times dt \times J\)

el = np.exp(-0.5j*(2 * DT * J) )
trotter_rzz_array = np.array([[el, 0, 0, 0],
                            [0, el.conj(), 0, 0],
                            [0, 0, el.conj(), 0],
                            [0, 0, 0, el]])

cudaq.register_operation("trotter_rzz", trotter_rzz_array)

We are now ready to define the time evolution circuit!

@cudaq.kernel
def trotterized_ising(
    N_qubits: int,
    flat_couplings: list[int],
    J: float,
    hx: float,
    dt: float,
    theta_init: float,
    n_trotter_steps: int,
    trotter_order: int,
):  
    # Prepare the initial state where every qubit 
    # is in the state cos(theta_init)|0> + sin(theta_init)|1>
    qubits = cudaq.qvector(N_qubits)
    rx(theta_init, qubits)

    # Order 1 Trotterization
    if trotter_order == 1:
        for r in range(n_trotter_steps):
            for i in range(0, len(flat_couplings), 2):
                q_i, q_j = qubits[flat_couplings[i]], qubits[flat_couplings[i+1]]
                trotter_rzz( q_i, q_j)
            for j in range(N_qubits):
                rx(2 * dt * hx, qubits[j])
    
    # Order 2 Trotterization
    elif trotter_order == 2:
        for j in range(N_qubits):
            rx(2 * dt * hx/2, qubits[j])
        for r in range(n_trotter_steps):
            for i in range(0, len(flat_couplings), 2):
                q_i, q_j = qubits[flat_couplings[i]], qubits[flat_couplings[i+1]]
                trotter_rzz( q_i, q_j)
            for j in range(N_qubits):
                rx(2 * dt * hx, qubits[j])
        for j in range(N_qubits):
            rx(-2 * dt * hx / 2, qubits[j])

We also need to define which spins in the lattice are coupled. We use a nearest-neighbor coupling with periodic boundary conditions.

def get_couplings():
    """Define the couplings for the 2D Ising model.
    
    The ordering of the qubits in the grid is row-major, i.e.
    for Lx=4, Ly=3 the qubits are ordered as follows:
    
    0  1  2  3
    4  5  6  7
    8  9  10 11

    The coupling is defined between nearest neighbors with periodic boundary. In the
    example above, qubit 0 is coupled to qubits 1, 3, 4, and 8.

    Returns
    -------
    couplings : list[tuple[int, int]]
        List of pairs of indices of the qubits that are coupled
    """
    couplings = []
    for i in range(LX):
        for j in range(LY):
            q_i = i * LY + j
            q_i_right = i * LY + (j + 1) % LY
            q_i_down = ((i + 1) % LX) * LY + j
            couplings.extend([[q_i, q_i_right], [q_i, q_i_down]])
    
    return couplings

This should be enough to run the CUDA-Q circuit. Let’s try it!

couplings = get_couplings()
# CUDA-Q does not support nested lists, so we flatten the list of couplings
flat_couplings = [index for pair in couplings for index in pair]

result = cudaq.sample(
    trotterized_ising,
    N_QUBITS,
    flat_couplings,
    J,
    hx,
    DT,
    THETA_INIT,
    N_TROTTER_STEPS,
    TROTTER_ORDER,
    shots_count= 1000,
)

print(result)

It is also possible to compute the expectation values of observables. The Hamiltonian we used for the time evolution is a natural choice.

from cudaq import spin

hamiltonian = 0.

# Transverse field terms
for i in range(N_QUBITS):
    hamiltonian += hx * spin.x(i) 

# Coupling terms
for i,j in couplings:
    hamiltonian += J * spin.z(i) * spin.z(j)

exp_val = cudaq.observe(
    trotterized_ising,
    hamiltonian,
    N_QUBITS,
    flat_couplings,
    J,
    hx,
    DT,
    THETA_INIT,
    N_TROTTER_STEPS,
    TROTTER_ORDER,
).expectation()

print(exp_val)