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.
Install the CUDA-Q Python package
Save your credentials in the environment variables
FERMIONIQ_ACCESS_TOKEN_ID
andFERMIONIQ_ACCESS_TOKEN_SECRET
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)