Source code for qvartools._ext.cudaq_vqe

"""CUDA-QX VQE and ADAPT-VQE pipeline wrapper.

Provides GPU-accelerated VQE using NVIDIA CUDA-Q + CUDA-QX Solvers.
Supports VQE with UCCSD ansatz and ADAPT-VQE with spin-complement GSD
operator pool.

Usage::

    from qvartools._ext.cudaq_vqe import run_cudaq_vqe
    result = run_cudaq_vqe(
        geometry=[("H", (0., 0., 0.)), ("H", (0., 0., 0.7474))],
        basis="sto-3g",
        method="adapt-vqe",
    )
    print(result["energy"], result["error_mha"])

Requires: cudaq >= 0.13, cudaq-solvers >= 0.5
"""

from __future__ import annotations

import logging
import math
import time
from typing import Any

logger = logging.getLogger(__name__)

_VALID_METHODS = {"vqe", "adapt-vqe"}


[docs] def run_cudaq_vqe( geometry: list[tuple[str, tuple[float, float, float]]], basis: str = "sto-3g", charge: int = 0, spin: int = 0, method: str = "vqe", optimizer: str = "cobyla", max_iterations: int = 200, target: str = "nvidia", nele_cas: int | None = None, norb_cas: int | None = None, gate_fusion: int = 4, verbose: bool = False, ) -> dict[str, Any]: """Run VQE or ADAPT-VQE using CUDA-QX Solvers on GPU. Parameters ---------- geometry : list of (str, (float, float, float)) Molecular geometry in Angstroms. basis : str Gaussian basis set (default ``"sto-3g"``). charge : int Net charge (default ``0``). spin : int Number of unpaired electrons, i.e. ``2S`` (default ``0`` for singlet). This matches PySCF convention: 0 = singlet, 1 = doublet, 2 = triplet. method : str ``"vqe"`` for VQE-UCCSD or ``"adapt-vqe"`` for ADAPT-VQE. optimizer : str Optimizer name (default ``"cobyla"``). Applied to both VQE and ADAPT-VQE. max_iterations : int Maximum optimizer iterations. target : str CUDA-Q simulator target (default ``"nvidia"`` for GPU). Use ``"qpp-cpu"`` for CPU-only environments. nele_cas : int or None, optional Number of active-space electrons. If ``None`` (default), all electrons are included. Setting this reduces qubit count and speeds up computation for large molecules. norb_cas : int or None, optional Number of active-space orbitals. If ``None`` (default), all orbitals are included. Must be set together with *nele_cas*. gate_fusion : int Maximum qubit count for gate fusion optimization (default ``4``). CUDA-Q combines consecutive gates involving up to this many qubits into a single matrix operation. Set ``0`` to disable. verbose : bool Print progress. Returns ------- dict ``energy`` : float — final VQE/ADAPT-VQE energy (Ha). ``fci_energy`` : float or None — CASCI FCI reference. ``hf_energy`` : float or None — Hartree-Fock reference. ``error_mha`` : float or None — error vs FCI (mHa). ``wall_time`` : float — wall-clock time (s). ``n_params`` : int — number of optimized parameters. ``iterations`` : int — optimizer iterations. ``method`` : str — ``"vqe"`` or ``"adapt-vqe"``. ``n_qubits`` : int — problem qubit count. ``n_electrons`` : int — active-space electron count. ``optimal_parameters`` : list[float] — optimal variational params. Raises ------ ValueError If *method* is not ``"vqe"`` or ``"adapt-vqe"``. RuntimeError If VQE fails to converge (energy is NaN or inf). Notes ----- ``fci_energy`` is the CASCI FCI energy within the active space, not the full-space FCI. For small molecules with minimal basis (e.g. H2/sto-3g), the active space equals the full space. """ import os if method not in _VALID_METHODS: raise ValueError(f"method must be one of {_VALID_METHODS}, got {method!r}") if (nele_cas is None) != (norb_cas is None): raise ValueError( "nele_cas and norb_cas must both be set or both be None; " f"got nele_cas={nele_cas}, norb_cas={norb_cas}" ) # Enable gate fusion before any cudaq import/kernel compilation if gate_fusion > 0: os.environ["CUDAQ_FUSION_MAX_QUBITS"] = str(gate_fusion) import cudaq import cudaq_solvers as solvers try: cudaq.set_target(target) except RuntimeError: logger.warning("Failed to set target '%s', falling back to qpp-cpu", target) cudaq.set_target("qpp-cpu") mol_kwargs: dict[str, Any] = { "geometry": geometry, "basis": basis, "spin": spin, "charge": charge, "casci": True, } if nele_cas is not None and norb_cas is not None: mol_kwargs["nele_cas"] = nele_cas mol_kwargs["norb_cas"] = norb_cas logger.info( "Active space: %d electrons in %d orbitals (%d qubits)", nele_cas, norb_cas, norb_cas * 2, ) molecule = solvers.create_molecule(**mol_kwargs) n_qubits: int = molecule.n_orbitals * 2 n_electrons: int = molecule.n_electrons hamiltonian = molecule.hamiltonian hf_energy: float | None = molecule.energies.get("hf_energy", None) fci_energy: float | None = molecule.energies.get("fci_energy", None) logger.info( "CUDA-QX %s: %d qubits, %d electrons, basis=%s", method, n_qubits, n_electrons, basis, ) t0 = time.perf_counter() if method == "adapt-vqe": energy, params, n_params, iterations = _run_adapt_vqe( molecule=molecule, n_electrons=n_electrons, hamiltonian=hamiltonian, optimizer=optimizer, max_iterations=max_iterations, verbose=verbose, ) else: energy, params, n_params, iterations = _run_vqe_uccsd( n_qubits=n_qubits, n_electrons=n_electrons, spin=spin, hamiltonian=hamiltonian, optimizer=optimizer, max_iterations=max_iterations, verbose=verbose, ) wall_time = time.perf_counter() - t0 if not math.isfinite(energy): raise RuntimeError( f"{method} failed to converge: energy={energy}. " f"Try increasing max_iterations or using a different optimizer." ) error_mha: float | None = ( (energy - fci_energy) * 1000.0 if fci_energy is not None else None ) return { "energy": energy, "fci_energy": fci_energy, "hf_energy": hf_energy, "error_mha": error_mha, "wall_time": wall_time, "n_params": n_params, "iterations": iterations, "method": method, "n_qubits": n_qubits, "n_electrons": n_electrons, "optimal_parameters": params, }
def _run_vqe_uccsd( n_qubits: int, n_electrons: int, spin: int, hamiltonian: Any, optimizer: str, max_iterations: int, verbose: bool, ) -> tuple[float, list[float], int, int]: """VQE with UCCSD ansatz.""" import cudaq import cudaq_solvers as solvers num_params = solvers.stateprep.get_num_uccsd_parameters(n_electrons, n_qubits) _nq = n_qubits _ne = n_electrons _spin = spin @cudaq.kernel def uccsd_kernel(thetas: list[float]): q = cudaq.qvector(_nq) for i in range(_ne): x(q[i]) solvers.stateprep.uccsd(q, thetas, _ne, _spin) energy, params, data = solvers.vqe( uccsd_kernel, hamiltonian, initial_parameters=[0.0] * num_params, optimizer=optimizer, max_iterations=max_iterations, verbose=verbose, ) return energy, list(params), num_params, len(data) def _run_adapt_vqe( molecule: Any, n_electrons: int, hamiltonian: Any, optimizer: str, max_iterations: int, verbose: bool, ) -> tuple[float, list[float], int, int]: """ADAPT-VQE with spin-complement GSD operator pool.""" import cudaq import cudaq_solvers as solvers operators = solvers.get_operator_pool( "spin_complement_gsd", num_orbitals=molecule.n_orbitals, ) @cudaq.kernel def initial_state(q: cudaq.qview): for i in range(n_electrons): x(q[i]) energy, params, ops = solvers.adapt_vqe( initial_state, hamiltonian, operators, optimizer=optimizer, max_iter=max_iterations, verbose=verbose, ) return energy, list(params), len(params), len(ops)