VQE -- Variational Quantum Eigensolver
Find the ground state energy of a molecular Hamiltonian using the Variational Quantum Eigensolver algorithm with pyqpanda3.
Problem
Finding Ground State Energy
A central problem in quantum chemistry and condensed matter physics is determining the ground state energy of a quantum system. Given a Hamiltonian
Classical methods such as Full Configuration Interaction (FCI) provide exact results but scale exponentially with system size: exact diagonalization of an
The electronic structure Hamiltonian in second quantization is:
where
Classical Difficulty of Exact Diagonalization
For a molecule with
- H
(2 electrons, 4 spin-orbitals): FCI dimension is , trivial to diagonalize. - H
O (10 electrons, 14 spin-orbitals): FCI dimension is , manageable. - FeMoco (active space): FCI dimension exceeds
, intractable for exact methods.
Even with symmetry reductions and sparse matrix techniques, classical exact diagonalization hits a wall around 20~30 spin-orbitals. This is precisely where quantum computers may offer an advantage.
The Variational Principle
The Variational Quantum Eigensolver exploits the variational principle of quantum mechanics. For any trial state
The expectation value of the Hamiltonian with respect to any trial state is an upper bound on the true ground state energy. By minimizing
The closer the trial state
Why VQE?
VQE, introduced by Peruzzo et al. in 2014, is a hybrid quantum-classical algorithm designed for near-term quantum hardware:
- Shallow circuits -- the quantum device only needs to prepare a parameterized state and measure observables, not perform the full diagonalization.
- Robust to noise -- because measurements are repeated many times with short circuits, VQE is more tolerant of gate errors than algorithms requiring deep coherence.
- Chemically relevant -- even coarse approximations of
are valuable for predicting reaction rates, binding energies, and molecular stability. - Scalable architecture -- the quantum and classical components can be improved independently, allowing incremental advances in both.
Solution
Step-by-Step VQE Workflow
Step 1: Construct the Molecular Hamiltonian
For the H
Step 2: Map to Qubits via Jordan-Wigner
Fermionic operators do not directly map to qubits. We transform them to Pauli operators using the Jordan-Wigner transformation:
For H
Step 3: Choose an Ansatz
An ansatz is the parameterized quantum circuit that prepares the trial state
- Hardware-efficient ansatz: alternating layers of single-qubit rotations and entangling gates. Flexible but may suffer from barren plateaus.
- Chemistry-inspired ansatz (e.g., Unitary Coupled Cluster): derived from classical chemistry methods. More physically motivated but deeper circuits.
Step 4: Measure Pauli Term Expectations
Decompose
Each
Step 5: Optimize Parameters
A classical optimizer (COBYLA, SLSQP, L-BFGS-B, etc.) adjusts
Code
H2 Molecule VQE with Hardware-Efficient Ansatz
We implement a complete VQE for the H
Define the H2 Hamiltonian
The coefficients below correspond to H
from pyqpanda3 import core
from pyqpanda3 import hamiltonian
import numpy as np
# H2 (STO-3G, R=0.735 A) coefficients after Jordan-Wigner mapping.
# These are the standard textbook values for the 2-qubit reduced Hamiltonian.
# The identity coefficient shifts the energy but does not affect optimization.
c0 = -1.0523792 # I (identity -- constant offset)
c1 = -0.39793742 # Z0
c2 = -0.39793742 # Z1
c3 = 0.18093190 # Z0 Z1
c4 = 0.39793742 # X0 X1
c5 = 0.01128010 # Y0 Y1
# Build the PauliOperator for the H2 Hamiltonian (excluding the identity term,
# which adds a constant offset to all energies and does not affect optimization).
h2_terms = {
"Z0": c1,
"Z1": c2,
"Z0 Z1": c3,
"X0 X1": c4,
"Y0 Y1": c5,
}
h2_hamiltonian = hamiltonian.PauliOperator(h2_terms)
identity_offset = c0
print("H2 Hamiltonian terms:")
for term, coeff in h2_terms.items():
print(f" {term}: {coeff:+.8f}")
print(f" Identity offset: {identity_offset:+.8f}")
print(f" Total terms (excl. identity): {len(h2_terms)}")Build the Hardware-Efficient Ansatz
A hardware-efficient ansatz for 2 qubits consists of parameterized single-qubit rotations followed by entangling gates. We use an RY-CNOT-RY structure:
def build_hwe_ansatz(theta: list, n_qubits: int = 2, n_layers: int = 1) -> core.QProg:
"""Build a hardware-efficient ansatz for VQE.
Structure per layer:
1. RY rotation on each qubit
2. CNOT ladder between adjacent qubits
3. RY rotation on each qubit
Args:
theta: Flat list of rotation angles.
n_qubits: Number of qubits.
n_layers: Number of repeated layers.
Returns:
QProg containing the parameterized ansatz.
"""
prog = core.QProg()
idx = 0
for layer in range(n_layers):
# First block of RY rotations
for q in range(n_qubits):
prog << core.RY(q, theta[idx])
idx += 1
# Entangling layer: CNOT ladder
for q in range(n_qubits - 1):
prog << core.CNOT(q, q + 1)
# Second block of RY rotations
for q in range(n_qubits):
prog << core.RY(q, theta[idx])
idx += 1
return prog
# Compute the number of parameters required
n_qubits = 2
n_layers = 1
n_params = 2 * n_qubits * n_layers # 2 rotations per qubit per layer
print(f"Number of variational parameters: {n_params}")Use Statevector for Exact Expectation
For small systems, we can avoid shot noise by using the statevector-based expectation function provided by pyqpanda3. This is the recommended approach for benchmarking and validation:
def compute_vqe_energy_exact(theta: list, h_op: hamiltonian.PauliOperator,
offset: float, n_qubits: int = 2) -> float:
"""Compute VQE energy using exact statevector expectation.
This is noise-free and deterministic, suitable for benchmarking
and small-scale validation.
Args:
theta: Variational parameters.
h_op: PauliOperator (without identity).
offset: Identity coefficient.
n_qubits: Number of qubits.
Returns:
Exact energy E(theta).
"""
prog = build_hwe_ansatz(theta, n_qubits)
expectation = core.expval_pauli_operator(prog, h_op)
return expectation + offset
# Test at initial parameters theta = [0, 0, 0, 0]
test_theta = [0.0, 0.0, 0.0, 0.0]
test_energy = compute_vqe_energy_exact(
test_theta, h2_hamiltonian, identity_offset, n_qubits
)
print(f"Exact energy at theta=[0,0,0,0]: {test_energy:.6f} Ha")Compute Expectation Values via Shot-Based Measurement
The energy expectation is a weighted sum of individual Pauli term expectations. For terms involving only
def measure_pauli_expectation(prog: core.QProg, pauli_term: str,
n_qubits: int, shots: int = 8192) -> float:
"""Measure the expectation value of a single Pauli string.
For Z operators, measure directly. For X, prepend H. For Y, prepend
RX(pi/2) to rotate into the computational basis.
Args:
prog: The ansatz circuit (without measurement).
pauli_term: Pauli string like "X0 X1" or "Z0 Z1".
n_qubits: Total number of qubits.
shots: Number of measurement shots.
Returns:
Expectation value <P> in [-1, 1].
"""
# Parse the Pauli term into a dict: {qubit_index: 'X'|'Y'|'Z'}
pauli_dict = {}
for token in pauli_term.strip().split():
if len(token) >= 2:
pauli_dict[int(token[1:])] = token[0]
# Build measurement circuit: ansatz + basis rotation + measurement
meas_prog = core.QProg()
meas_prog << prog
# Apply basis rotations for non-Z measurements
for qubit, pauli in pauli_dict.items():
if pauli == 'X':
meas_prog << core.H(qubit)
elif pauli == 'Y':
meas_prog << core.RX(qubit, np.pi / 2)
# Measure all qubits
meas_prog << core.measure(
list(range(n_qubits)), list(range(n_qubits))
)
# Run and get counts
machine = core.CPUQVM()
machine.run(meas_prog, shots)
counts = machine.result().get_counts()
# Compute expectation value
# Each qubit contributes +1 for |0> and -1 for |1>
expectation = 0.0
for bitstring, count in counts.items():
eigenvalue = 1.0
for qubit, pauli in pauli_dict.items():
if bitstring[qubit] == '1':
eigenvalue *= -1.0
expectation += eigenvalue * count
return expectation / shotsCompute Total Energy from the Hamiltonian
def compute_vqe_energy_shots(theta: list, h_terms: dict,
offset: float, n_qubits: int = 2,
shots: int = 8192) -> float:
"""Compute the total VQE energy using shot-based measurement.
Args:
theta: Variational parameters for the ansatz.
h_terms: Dictionary mapping Pauli strings to coefficients.
offset: Constant energy offset (identity coefficient).
n_qubits: Number of qubits.
shots: Measurement shots per Pauli term.
Returns:
Total energy estimate E(theta) = offset + sum c_i <P_i>.
"""
prog = build_hwe_ansatz(theta, n_qubits)
energy = offset
for key, coeff in h_terms.items():
pauli_exp = measure_pauli_expectation(prog, key, n_qubits, shots)
energy += coeff * pauli_exp
return energyOptimize with scipy.optimize (COBYLA)
COBYLA (Constrained Optimization BY Linear Approximation) is a gradient-free optimizer well-suited for noisy objective functions like shot-based VQE:
from scipy.optimize import minimize
# Keys for iterating over Pauli terms
pauli_keys = list(h2_terms.keys())
def objective(theta):
"""VQE objective function for the optimizer (exact statevector)."""
return compute_vqe_energy_exact(
theta.tolist() if hasattr(theta, 'tolist') else theta,
h2_hamiltonian, identity_offset, n_qubits
)
# Run optimization with multiple random restarts
best_energy = float('inf')
best_theta = None
convergence_history = []
n_restarts = 10
for restart in range(n_restarts):
np.random.seed(restart)
theta0 = np.random.uniform(0, 2 * np.pi, n_params)
# Track convergence for this restart
history = []
def objective_with_history(theta):
val = objective(theta)
history.append(val)
return val
result = minimize(
objective_with_history,
theta0,
method='COBYLA',
options={'maxiter': 500, 'rhobeg': 0.5}
)
if result.fun < best_energy:
best_energy = result.fun
best_theta = result.x.copy()
convergence_history = history
print(f" Restart {restart}: energy = {result.fun:.8f} Ha")
print(f"\nBest VQE energy: {best_energy:.8f} Ha")
print(f"Optimal theta: {best_theta}")Expected output:
Restart 0: energy = -1.13727970 Ha
...
Restart 7: energy = -1.13728383 Ha
...
Best VQE energy: -1.13728383 Ha
Optimal theta: [...]Track and Plot Convergence History
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(range(len(convergence_history)), convergence_history,
'b-', linewidth=0.8, alpha=0.7)
ax.axhline(y=best_energy, color='r', linestyle='--',
label=f'Best E = {best_energy:.6f} Ha')
ax.set_xlabel('Function Evaluation')
ax.set_ylabel('Energy (Hartree)')
ax.set_title('VQE Convergence History')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('vqe_convergence.png', dpi=150)
plt.show()
print("Convergence plot saved to vqe_convergence.png")Compare with Exact Diagonalization
To validate the VQE result, we compute the exact ground state energy by diagonalizing the full Hamiltonian matrix:
# Build the full 2-qubit Hamiltonian matrix
# 4x4 matrix for 2 qubits
def build_hamiltonian_matrix(c0, c1, c2, c3, c4, c5):
"""Build the full matrix representation of the H2 Hamiltonian."""
I2 = np.eye(2)
X = np.array([[0, 1], [1, 0]])
Y = np.array([[0, -1j], [1j, 0]])
Z = np.array([[1, 0], [0, -1]])
H_matrix = (
c0 * np.kron(I2, I2)
+ c1 * np.kron(Z, I2)
+ c2 * np.kron(I2, Z)
+ c3 * np.kron(Z, Z)
+ c4 * np.kron(X, X)
+ c5 * np.kron(Y, Y)
)
return H_matrix
H_matrix = build_hamiltonian_matrix(c0, c1, c2, c3, c4, c5)
eigenvalues = np.linalg.eigvalsh(H_matrix)
exact_energy = eigenvalues[0]
print(f"Exact ground state energy (diagonalization): {exact_energy:.8f} Ha")
print(f"VQE best energy: {best_energy:.8f} Ha")
print(f"Error: {best_energy - exact_energy:.8f} Ha")
print(f"All eigenvalues: {eigenvalues}")Expected output:
Exact ground state energy (diagonalization): -1.13728383 Ha
VQE best energy: -1.13728383 Ha
Error: ~0.00000000 HaThe hardware-efficient ansatz with 4 parameters is expressive enough to represent the exact ground state of the 2-qubit H
General VQE Solver Function
The following function encapsulates the complete VQE workflow for any 2-qubit Hamiltonian:
def solve_vqe(
h_terms: dict,
offset: float,
n_qubits: int = 2,
n_layers: int = 1,
optimizer: str = 'COBYLA',
n_restarts: int = 10,
maxiter: int = 500,
) -> dict:
"""Run VQE to find the ground state energy of a Hamiltonian.
Args:
h_terms: Dictionary mapping Pauli strings to coefficients.
offset: Constant energy offset (identity term).
n_qubits: Number of qubits.
n_layers: Number of ansatz layers.
optimizer: scipy optimizer name ('COBYLA', 'SLSQP', 'L-BFGS-B').
n_restarts: Number of random restarts.
maxiter: Maximum optimizer iterations per restart.
Returns:
Dictionary with keys:
- energy: best VQE energy found
- theta: optimal parameters
- n_evals: total function evaluations
"""
h_op = hamiltonian.PauliOperator(h_terms)
n_params = 2 * n_qubits * n_layers
best_energy = float('inf')
best_theta = None
total_evals = 0
for restart in range(n_restarts):
np.random.seed(restart)
theta0 = np.random.uniform(0, 2 * np.pi, n_params)
def obj(theta):
prog = build_hwe_ansatz(theta.tolist(), n_qubits, n_layers)
return core.expval_pauli_operator(prog, h_op) + offset
result = minimize(
obj, theta0, method=optimizer,
options={'maxiter': maxiter, 'rhobeg': 0.5}
)
total_evals += result.nfev
if result.fun < best_energy:
best_energy = result.fun
best_theta = result.x.copy()
return {
'energy': best_energy,
'theta': best_theta,
'n_evals': total_evals,
}
# Solve H2
result_vqe = solve_vqe(h2_terms, identity_offset, n_qubits=2, n_layers=1)
print(f"VQE result: E = {result_vqe['energy']:.8f} Ha")
print(f"Exact: E = {exact_energy:.8f} Ha")
print(f"Error: {result_vqe['energy'] - exact_energy:.2e} Ha")VQE for Different Bond Lengths -- Potential Energy Surface
A key application of VQE is computing the potential energy surface (PES) of a molecule by running VQE at different internuclear distances:
# H2 Hamiltonian coefficients at different bond lengths (STO-3G, 2 qubits)
# These values come from classical electronic structure calculations
bond_data = {
0.50: {'Z0': -0.491888, 'Z1': -0.491888, 'Z0 Z1': 0.223432,
'X0 X1': 0.397937, 'Y0 Y1': 0.011280, 'I': -0.728376},
0.60: {'Z0': -0.452892, 'Z1': -0.452892, 'Z0 Z1': 0.201544,
'X0 X1': 0.397937, 'Y0 Y1': 0.011280, 'I': -0.878857},
0.74: {'Z0': -0.397937, 'Z1': -0.397937, 'Z0 Z1': 0.180932,
'X0 X1': 0.397937, 'Y0 Y1': 0.011280, 'I': -1.052379},
0.90: {'Z0': -0.347010, 'Z1': -0.347010, 'Z0 Z1': 0.163212,
'X0 X1': 0.397937, 'Y0 Y1': 0.011280, 'I': -1.215771},
1.20: {'Z0': -0.281144, 'Z1': -0.281144, 'Z0 Z1': 0.139468,
'X0 X1': 0.397937, 'Y0 Y1': 0.011280, 'I': -1.445068},
1.60: {'Z0': -0.229260, 'Z1': -0.229260, 'Z0 Z1': 0.120888,
'X0 X1': 0.397937, 'Y0 Y1': 0.011280, 'I': -1.670525},
2.00: {'Z0': -0.195699, 'Z1': -0.195699, 'Z0 Z1': 0.107893,
'X0 X1': 0.397937, 'Y0 Y1': 0.011280, 'I': -1.856666},
}
print(f"{'R (A)':>8s} {'E_VQE (Ha)':>12s} {'E_exact (Ha)':>13s} {'Error':>10s}")
print("-" * 50)
vqe_energies = []
exact_energies = []
bond_lengths = sorted(bond_data.keys())
for R in bond_lengths:
data = bond_data[R]
terms = {k: v for k, v in data.items() if k != 'I'}
id_offset = data['I']
# VQE
vqe_result = solve_vqe(terms, id_offset, n_qubits=2, n_layers=1, n_restarts=5)
# Exact diagonalization
I2 = np.eye(2)
X = np.array([[0, 1], [1, 0]])
Y = np.array([[0, -1j], [1j, 0]])
Z = np.array([[1, 0], [0, -1]])
H = data['I'] * np.kron(I2, I2)
H += data['Z0'] * np.kron(Z, I2)
H += data['Z1'] * np.kron(I2, Z)
H += data['Z0 Z1'] * np.kron(Z, Z)
H += data['X0 X1'] * np.kron(X, X)
H += data['Y0 Y1'] * np.kron(Y, Y)
e_exact = np.linalg.eigvalsh(H)[0]
vqe_energies.append(vqe_result['energy'])
exact_energies.append(e_exact)
error = vqe_result['energy'] - e_exact
print(f"{R:8.2f} {vqe_result['energy']:12.8f} {e_exact:13.8f} {error:+10.2e}")Expected output:
R (A) E_VQE (Ha) E_exact (Ha) Error
--------------------------------------------------
0.50 -0.88282600 -0.88282600 +0.00e+00
0.60 -1.09465100 -1.09465100 +0.00e+00
0.74 -1.13728383 -1.13728383 +0.00e+00
0.90 -1.11134900 -1.11134900 +0.00e+00
1.20 -1.02522100 -1.02522100 +0.00e+00
1.60 -0.96366900 -0.96366900 +0.00e+00
2.00 -0.93425800 -0.93425800 +0.00e+00Plotting the Potential Energy Surface
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(bond_lengths, exact_energies, 'ro-', label='Exact (FCI)', markersize=6)
ax.plot(bond_lengths, vqe_energies, 'bx--', label='VQE (HWE ansatz)', markersize=8)
ax.set_xlabel('Bond Length R (Angstrom)')
ax.set_ylabel('Energy (Hartree)')
ax.set_title('H2 Potential Energy Surface: VQE vs Exact')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('vqe_pes_h2.png', dpi=150)
plt.show()
print("PES plot saved to vqe_pes_h2.png")Extending to LiH (4-Qubit Example)
For larger molecules, the Hamiltonian requires more qubits. Lithium hydride (LiH) in the STO-3G basis with 4 active spin-orbitals maps to a 4-qubit problem. The reduced 4-qubit Hamiltonian (after freezing core orbitals and removing symmetries) has the following form:
# LiH (STO-3G, R=1.546 A) -- 4-qubit reduced Hamiltonian coefficients
# After Jordan-Wigner transformation with frozen core and symmetry reduction.
lih_terms = {
"Z0": -0.102629, "Z1": -0.102629,
"Z0 Z1": 0.161024, "Z0 Z2": 0.124752, "Z0 Z3": 0.124752,
"Z1 Z2": 0.124752, "Z1 Z3": 0.124752,
"Z2 Z3": 0.171134,
"X0 X1": 0.044089, "X0 X2": 0.011280, "X0 X3": 0.011280,
"X1 X2": 0.011280, "X1 X3": 0.011280,
"X2 X3": 0.073553,
"Y0 Y2": -0.011280, "Y0 Y3": -0.011280,
"Y1 Y2": -0.011280, "Y1 Y3": -0.011280,
"Y0 Y1": 0.044089,
"Y2 Y3": 0.073553,
}
lih_identity_offset = -0.653746 # constant shift
lih_n_qubits = 4
lih_n_layers = 2 # more layers for a more complex molecule
# Solve LiH VQE
lih_result = solve_vqe(
lih_terms, lih_identity_offset,
n_qubits=lih_n_qubits, n_layers=lih_n_layers,
n_restarts=15, maxiter=1000
)
# Exact diagonalization for validation
# Build the 4-qubit Hamiltonian matrix
def build_pauli_matrix(pauli_str: str, n_qubits: int) -> np.ndarray:
"""Build the matrix for a Pauli string on n qubits."""
I2 = np.eye(2)
X = np.array([[0, 1], [1, 0]])
Y = np.array([[0, -1j], [1j, 0]])
Z = np.array([[1, 0], [0, -1]])
pauli_map = {'I': I2, 'X': X, 'Y': Y, 'Z': Z}
# Parse the Pauli string into per-qubit operators
ops = ['I'] * n_qubits
for token in pauli_str.strip().split():
pauli_char = token[0]
qubit_idx = int(token[1:])
ops[qubit_idx] = pauli_char
# Build the matrix via Kronecker products
mat = pauli_map[ops[0]]
for i in range(1, n_qubits):
mat = np.kron(mat, pauli_map[ops[i]])
return mat
H_lih = lih_identity_offset * np.eye(2**lih_n_qubits)
for term_str, coeff in lih_terms.items():
H_lih += coeff * build_pauli_matrix(term_str, lih_n_qubits)
exact_lih = np.linalg.eigvalsh(H_lih)[0]
print(f"LiH VQE energy: {lih_result['energy']:.8f} Ha")
print(f"LiH exact energy: {exact_lih:.8f} Ha")
print(f"Error: {lih_result['energy'] - exact_lih:.8f} Ha")For the 4-qubit LiH problem, the hardware-efficient ansatz with 2 layers (16 parameters) typically achieves accuracy within chemical accuracy (
Explanation
Mathematical Foundation: The Variational Principle in Depth
The variational principle is a fundamental result of linear algebra applied to quantum mechanics. Given a Hermitian operator
The energy expectation is:
This inequality holds because all eigenvalues
Implications for VQE:
- Any trial state gives a valid upper bound on
. - The bound tightens as the trial state approaches
. - The quality of the ansatz determines how close we can get.
- Even a poor ansatz gives a useful upper bound.
The variational principle also provides a natural convergence criterion: if
Ansatz Design Considerations
The choice of ansatz is critical to VQE performance. Key considerations:
Hardware-Efficient Ansatz
The hardware-efficient ansatz used in this tutorial has the form:
Properties:
- Each layer adds
parameters for qubits. - The CNOT ladder provides entanglement.
rotations explore the Bloch sphere in the plane. - With enough layers, the ansatz can approximate any unitary (universal).
Chemistry-Inspired Ansatz (UCCSD)
The Unitary Coupled Cluster Singles and Doubles (UCCSD) ansatz is derived from classical coupled cluster theory:
where
UCCSD produces deeper circuits but encodes physical correlations, making it more reliable for reaching the true ground state. However, the number of parameters grows as
Expressibility vs Trainability Tradeoff
- Expressibility: the fraction of the Hilbert space that the ansatz can reach. More parameters generally increase expressibility.
- Trainability: whether the optimizer can find good parameters. Highly expressive ansatze may suffer from barren plateaus where gradients vanish exponentially.
The ideal ansatz is expressive enough to represent the ground state but structured enough to avoid barren plateaus. For chemistry problems, problem-inspired ansatze like UCCSD achieve this by encoding known physical structure.
Pauli Term Grouping for Efficient Measurement
The Hamiltonian decomposes into Pauli strings, and each string must be measured separately (or in compatible groups). The key insight is that Pauli operators in the same basis commute and can be measured simultaneously.
For our H
| Term | Measurement Basis | Rotation Required |
|---|---|---|
| Computational ( | None | |
| Computational ( | None | |
| Computational ( | None (commutes with | |
The
For larger molecules, more sophisticated grouping strategies exist:
- Qubit-wise commutativity (QWC): group terms that commute on each qubit independently. Reduces groups from
to roughly . - General commutativity: group terms that commute as matrices. Requires basis rotations that simultaneously diagonalize the group. Can further reduce the number of groups.
- Classical shadow tomography: use random measurements to estimate all Pauli expectations simultaneously with fewer total measurements.
Optimization Landscape and Barren Plateaus
Barren plateaus are a fundamental challenge in variational quantum algorithms. In a barren plateau, the gradient of the cost function vanishes exponentially with the number of qubits:
This means that for large systems, the energy landscape becomes exponentially flat, and gradient-based optimizers cannot distinguish the correct direction.
Causes of Barren Plateaus
- Expressibility: Highly expressive ansatze that cover the full Hilbert space uniformly produce flat landscapes. The energy expectation approaches the trace of
divided by , with exponentially small variance. - Entanglement: Deep entangling circuits that generate volume-law entanglement lead to barren plateaus. The gradient signal is diluted across the exponentially growing Hilbert space.
- Global cost functions: Using global cost functions (like
for a highly non-local ) exacerbates the problem. Local cost functions that measure observables on few qubits are more trainable. - Noise: Hardware noise flattens the landscape further, pushing the energy toward the maximally mixed state.
The identity-block initialization strategy starts each layer near the identity (small angles), so the initial circuit does almost nothing and the gradient landscape is well-behaved. The optimizer then gradually increases circuit expressibility as needed.
For H
Error Mitigation Considerations
Real quantum hardware introduces errors that shift the estimated energy. Common error mitigation techniques for VQE include:
Zero-Noise Extrapolation (ZNE)
Run the circuit at multiple noise levels (by stretching gate times or inserting identity-equivalent gate pairs) and extrapolate to zero noise:
where
Readout Error Mitigation
Calibrate the measurement error matrix
For
Symmetry Verification
Many molecular Hamiltonians have symmetries (particle number, spin, parity). After measurement, discard bitstrings that violate these symmetries. This "post-selection" removes a fraction of the noisy outcomes at the cost of reducing the effective number of shots.
Probabilistic Error Cancellation
Characterize each gate's noise channel and probabilistically apply inverse operations to cancel errors. This produces unbiased estimates but increases sampling overhead exponentially with circuit depth.
Scaling Analysis
The computational cost of VQE scales with the number of qubits (orbitals) and the number of Hamiltonian terms:
| Molecule | Qubits | Pauli Terms | Shots/term | Total shots |
|---|---|---|---|---|
| H | 2 | 5 | 10,000 | 50,000 |
| LiH (STO-3G) | 4 | ~100 | 10,000 | 1,000,000 |
| BeH | 6 | ~200 | 10,000 | 2,000,000 |
| H | 8 | ~600 | 10,000 | 6,000,000 |
| NH | 10 | ~2,000 | 10,000 | 20,000,000 |
The number of Pauli terms grows as
Comparison with Classical Methods
| Method | Scaling | Accuracy | Applicability |
|---|---|---|---|
| FCI (exact) | Exact | Small systems only ( | |
| CCSD(T) | ~1 mHa for weakly correlated | Gold standard, fails for strong correlation | |
| DFT | ~10 mHa (functional dependent) | Ubiquitous, but accuracy unpredictable | |
| VQE (UCCSD) | ~1 mHa (ansatz dependent) | Promising for strong correlation on QPU | |
| VQE (hardware-efficient) | Variable (depends on depth) | Near-term feasible, less predictable |
VQE is not intended to replace classical methods for small, weakly correlated molecules. Its value lies in tackling strongly correlated systems where classical methods break down: transition metal complexes, bond-breaking processes, and excited states with multi-reference character.
Classical Optimizer Comparison
Different optimizers have tradeoffs in the VQE context:
| Optimizer | Type | Gradient Needed | Noise Tolerant | Typical Iterations |
|---|---|---|---|---|
| COBYLA | Derivative-free | No | High | 200~1000 |
| Nelder-Mead | Derivative-free | No | Medium | 200~500 |
| SLSQP | Gradient-based | Estimated | Low | 50~200 |
| L-BFGS-B | Gradient-based | Estimated | Low | 30~100 |
| SPSA | Stochastic | Approximate | High | 500~5000 |
| Adam (quantum) | Gradient-based | Parameter shift | Medium | 100~500 |
For shot-based VQE, gradient-free optimizers (COBYLA, Nelder-Mead) are preferred because shot noise makes gradient estimation unreliable. For exact (statevector) VQE, gradient-based methods converge faster.
SPSA (Simultaneous Perturbation Stochastic Approximation) is particularly suited for hardware experiments because it requires only 2 circuit evaluations per iteration regardless of the number of parameters, compared to
Common Pitfalls and Debugging Tips
Pitfall 1: Wrong Hamiltonian Coefficients
The coefficients depend sensitively on the molecular geometry, basis set, and active space selection. Always validate against a known classical calculation (FCI or CCSD) before running VQE.
Debugging: Compute the exact ground state energy by diagonalizing the Hamiltonian matrix and compare with literature values. For H
Pitfall 2: Insufficient Ansatz Expressibility
If the ansatz cannot represent the ground state, VQE will converge to a value strictly above
Debugging: Increase the number of layers and observe whether the energy continues to decrease. If it plateaus well above
Pitfall 3: Local Minima in the Optimization Landscape
The VQE energy landscape is non-convex and can have multiple local minima.
Debugging: Use multiple random restarts (10~50) and compare the best energies. If different restarts converge to significantly different energies, the landscape has multiple minima and more restarts are needed.
Pitfall 4: Incorrect Measurement Basis Rotation
Forgetting to apply basis rotations before measurement gives wrong Pauli expectations.
Debugging: Verify each Pauli term individually against the statevector result before running the full optimization.
Pitfall 5: Insufficient Measurement Shots
Too few shots leads to noisy energy estimates that prevent convergence.
Debugging: Start with a large number of shots (10,000+) for validation, then reduce to find the minimum that still gives acceptable accuracy. The statistical error scales as
Pitfall 6: Identity Term Handling
The identity term
Debugging: Always compute and display the total energy including the identity offset when reporting absolute energies.
Summary
| Component | Implementation in pyqpanda3 |
|---|---|
| Hamiltonian | hamiltonian.PauliOperator({"Z0": -0.398, "X0 X1": 0.398, ...}) |
| Ansatz circuit | core.RY, core.CNOT, core.QProg |
| Expectation (exact) | core.expval_pauli_operator(prog, hamiltonian) |
| Shot-based measurement | core.CPUQVM().run(prog, shots) + result.get_counts() |
| Classical optimization | scipy.optimize.minimize with COBYLA/SLSQP |
| Exact diagonalization | numpy.linalg.eigvalsh(H_matrix) |
Next Steps
- QAOA Tutorial -- Variational algorithms for combinatorial optimization
- Noise Characterization -- Building realistic noise models for VQE on hardware
- Custom Gate -- Implementing specialized gates for chemistry-inspired ansatze