Adaptive Eigensolving with ADAPT-VQE
This tutorial demonstrates how to use ADAPT-VQE in Carcará to dynamically build compact quantum circuits for \(H_2\) and \(LiH\). We compare the hardware cost (CNOT gate counts and circuit depth) across four operator pools.
Running ADAPT-VQE on LiH
Unlike the fixed UCCSD ansatz, ADAPT-VQE selects operators from a pool one by one based on their energy gradient:
and grows the ansatz dynamically.
Here, we attach ADAPTVQE as an ASE calculator to solve for the ground state of \(LiH\) using the hardware-efficient Coupled-Exchange Operator ("ceo") pool:
from ase import Atoms
from carcara.algorithms import ADAPTVQE
# Setup LiH molecule in a cell
atoms = Atoms("LiH", positions=[[4.0, 4.0, 3.20], [4.0, 4.0, 4.80]],
cell=[[8.0, 0.0, 0.0], [0.0, 8.0, 0.0], [0.0, 0.0, 8.0]], pbc=True)
# Attach ADAPTVQE calculator
atoms.calc = ADAPTVQE(
pool="ceo",
basis="FAO",
mapping="jordan_wigner",
optimizer="COBYLA",
gradient="parameter-shift_rule",
h=0.18,
max_iterations=15,
gradient_tolerance=1e-5
)
# Run calculation (energy returned in eV)
energy_ev = atoms.get_total_energy()
result = atoms.calc.adapt_result
print(f"ADAPT-VQE Converged: {result.converged}")
print(f"Optimal Energy: {result.optimal_energy:.8f} Ha")
print(f"Number of Operators Growth: {result.num_operators}")
Comparing Operator Pools
Carcará provides four pools that trade off parameter freedom and circuit compilation complexity:
Pool Name |
Description |
Gate Count Trade-off |
|---|---|---|
|
Spin-adapted excitations |
Deep circuits due to Jordan-Wigner \(Z\)-string chains |
|
Individual mapped Pauli strings |
Shallow circuits per step, but higher total parameter count |
|
Qubit Excitation Basis |
\(Z\)-strings are omitted, yielding distance-independent entangling gates |
|
Coupled-Exchange Operators |
Shares entangling blocks, yielding maximum accuracy per CNOT |
We can run the comparative analysis across these pools on \(H_2\):
from ase import Atoms
from carcara.algorithms import ADAPTVQE
from carcara.optimizers import Optimizer
# H2 molecule
atoms = Atoms("H2", positions=[[4.0, 4.0, 3.63], [4.0, 4.0, 4.37]],
cell=[[8.0, 0.0, 0.0], [0.0, 8.0, 0.0], [0.0, 0.0, 8.0]], pbc=True)
pools = ["fermionic", "qubit", "qeb", "ceo"]
results = {}
for name in pools:
atoms.calc = ADAPTVQE(
pool=name,
basis="FAO",
h=0.20,
optimizer=Optimizer("L-BFGS-B", maxiter=2000),
max_iterations=10,
gradient_tolerance=1e-5,
verbose=False
)
atoms.get_total_energy()
results[name] = atoms.calc.adapt_result
# Print comparison
print(f"{'Pool':12s} | {'Energy (Ha)':>12s} | {'CNOT Count':>10s} | {'Depth':>8s}")
print("-" * 52)
for name in pools:
res = results[name]
print(f"{name:12s} | {res.optimal_energy:12.6f} | {res.metrics.cnot_count:10d} | {res.metrics.depth:8d}")
On \(H_2\) all pools converge to the exact ground state, but the hardware-optimized qubit-based pools achieve it with significantly fewer CNOTs (e.g., "qubit" requires only 6 CNOTs, whereas "fermionic" requires 48).