Generative Quantum Eigensolver (GQE)
The GQE algorithm has presented a novel approach to variational optimization that leverages generative AI. It is an active research topic. A core aspect of this research is with regards to the core cost / loss function the algorithm leverages. The current implementation provides a cost function suitable to small scale simulation. The GQE implementation in CUDA-Q Solvers is based on this paper: The generative quantum eigensolver (GQE) and its application for ground state search.
GQE Algorithm Overview:
Initialize or load a pre-trained generative model
Generate candidate quantum circuits
Evaluate circuit performance on target Hamiltonian
Update the generative model based on results
Repeat generation and optimization until convergence
CUDA-Q Solvers Implementation
CUDA-Q Solvers provides a high-level interface for running GQE simulations. Here’s how to use it:
# Run this script with
# python3 gqe_h2.py
#
# In order to leverage CUDA-Q MQPU and distribute the work across
# multiple QPUs (thereby observing a speed-up), set the target and
# use MPI:
#
# cudaq.set_target('nvidia', option='mqpu')
# cudaq.mpi.initialize()
#
# run with
#
# mpiexec -np N and vary N to see the speedup...
# e.g. mpiexec -np 2 python3 gqe_h2.py
#
# End the script with
# cudaq.mpi.finalize()
import numpy as np
import cudaq, cudaq_solvers as solvers
from cudaq import spin
from lightning.fabric.loggers import CSVLogger
from cudaq_solvers.gqe_algorithm.gqe import get_default_config
# Set deterministic seed and environment variables for deterministic behavior
# Disable this section for non-deterministic behavior
import os, torch
os.environ['CUBLAS_WORKSPACE_CONFIG'] = ':4096:8'
torch.manual_seed(3047)
torch.use_deterministic_algorithms(True)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
# Check if NVIDIA GPUs are available and set target accordingly
try:
cudaq.set_target('nvidia', option='fp64')
except RuntimeError:
# Fall back to CPU target
cudaq.set_target('qpp-cpu')
# Create the molecular hamiltonian
geometry = [('H', (0., 0., 0.)), ('H', (0., 0., .7474))]
molecule = solvers.create_molecule(geometry, 'sto-3g', 0, 0, casci=True)
spin_ham = molecule.hamiltonian
n_qubits = molecule.n_orbitals * 2
n_electrons = molecule.n_electrons
# Generate the operator pool
params = [
0.003125, -0.003125, 0.00625, -0.00625, 0.0125, -0.0125, 0.025, -0.025,
0.05, -0.05, 0.1, -0.1
]
def pool(params):
ops = []
i = 0
ops.append(
cudaq.SpinOperator(
spin.y(i) * spin.z(i + 1) * spin.x(i + 2) * spin.i(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.x(i) * spin.z(i + 1) * spin.y(i + 2) * spin.i(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.i(i) * spin.y(i + 1) * spin.z(i + 2) * spin.x(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.i(i) * spin.x(i + 1) * spin.z(i + 2) * spin.y(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.x(i) * spin.x(i + 1) * spin.x(i + 2) * spin.y(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.x(i) * spin.x(i + 1) * spin.y(i + 2) * spin.x(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.x(i) * spin.y(i + 1) * spin.y(i + 2) * spin.y(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.y(i) * spin.x(i + 1) * spin.y(i + 2) * spin.y(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.x(i) * spin.y(i + 1) * spin.x(i + 2) * spin.x(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.y(i) * spin.x(i + 1) * spin.x(i + 2) * spin.x(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.y(i) * spin.y(i + 1) * spin.x(i + 2) * spin.y(i + 3)))
ops.append(
cudaq.SpinOperator(
spin.y(i) * spin.y(i + 1) * spin.y(i + 2) * spin.x(i + 3)))
pool = []
for c in params:
for op in ops:
pool.append(c * op)
return pool
op_pool = pool(params)
def term_coefficients(op: cudaq.SpinOperator) -> list[complex]:
return [term.evaluate_coefficient() for term in op]
def term_words(op: cudaq.SpinOperator) -> list[cudaq.pauli_word]:
return [term.get_pauli_word(n_qubits) for term in op]
# Kernel that applies the selected operators
@cudaq.kernel
def kernel(n_qubits: int, n_electrons: int, coeffs: list[float],
words: list[cudaq.pauli_word]):
q = cudaq.qvector(n_qubits)
for i in range(n_electrons):
x(q[i])
for i in range(len(coeffs)):
exp_pauli(coeffs[i], q, words[i])
def cost(sampled_ops: list[cudaq.SpinOperator], **kwargs):
full_coeffs = []
full_words = []
for op in sampled_ops:
full_coeffs += [c.real for c in term_coefficients(op)]
full_words += term_words(op)
# If using CUDA-Q MQPU,
# use observe_async with the provided qpu_id:
# handle = cudaq.observe_async(kernel,
# spin_ham,
# n_qubits,
# n_electrons,
# full_coeffs,
# full_words,
# qpu_id=kwargs['qpu_id'])
# return handle, lambda res: res.get().expectation()
return cudaq.observe(kernel, spin_ham, n_qubits, n_electrons, full_coeffs,
full_words).expectation()
# Configure GQE
cfg = get_default_config()
cfg.use_fabric_logging = True
logger = CSVLogger("gqe_h2_logs/gqe.csv")
cfg.fabric_logger = logger
cfg.save_trajectory = True
cfg.verbose = True
# Run GQE
minE, best_ops = solvers.gqe(cost, op_pool, max_iters=25, ngates=10, config=cfg)
print(f'Ground Energy = {minE}')
print('Ansatz Ops')
for idx in best_ops:
# Get the first (and only) term since these are simple operators
term = next(iter(op_pool[idx]))
print(term.evaluate_coefficient().real, term.get_pauli_word(n_qubits))
The CUDA-Q Solvers implementation of GQE provides a flexible framework for adaptive circuit construction and optimization. The algorithm can efficiently utilize multiple QPUs through MPI for parallel operator evaluation, making it suitable for larger quantum systems.
Note
The GQE implementation is a Python-only implementation.