|
| 1 | +# Positive control: VQE implementation validation |
| 2 | +# Developed with help from Claude (Anthropic) |
| 3 | +# |
| 4 | +# Purpose: Demonstrate that the VQE implementation in clifford_vqe_ig_entangled.py |
| 5 | +# is capable of finding energy improvements when the bootstrap starting point is |
| 6 | +# deliberately wrong. This addresses the referee question: |
| 7 | +# "How do we know VQE would ever return a non-null variational update?" |
| 8 | +# |
| 9 | +# Method: Run VQE on H2 at equilibrium (STO-3G, 4 qubits) starting from: |
| 10 | +# (a) all-zeros state (vacuum, definitely wrong) |
| 11 | +# (b) random state |
| 12 | +# (c) correct bootstrap state (should already be near ground state) |
| 13 | +# |
| 14 | +# Expected result: |
| 15 | +# (a) and (b): VQE finds substantial improvement toward known ground state energy |
| 16 | +# (c): VQE finds little or no improvement (consistent with main paper results) |
| 17 | +# |
| 18 | +# Reference: H2 STO-3G FCI ground state energy ~ -1.1373 Ha |
| 19 | +# Hartree-Fock energy ~ -1.1175 Ha |
| 20 | + |
| 21 | +from openfermion import MolecularData, FermionOperator, jordan_wigner, get_fermion_operator |
| 22 | +from openfermionpyscf import run_pyscf |
| 23 | + |
| 24 | +import itertools |
| 25 | +import multiprocessing |
| 26 | +import numpy as np |
| 27 | +import os |
| 28 | +import random |
| 29 | + |
| 30 | +import pennylane as qml |
| 31 | +from pennylane import numpy as nppl |
| 32 | + |
| 33 | + |
| 34 | +# ── molecule: H2 at equilibrium ────────────────────────────────────────────── |
| 35 | + |
| 36 | +basis = "sto-3g" |
| 37 | +multiplicity = 1 |
| 38 | +charge = 0 |
| 39 | + |
| 40 | +geometry = [("H", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 0.74))] # H2 equilibrium |
| 41 | + |
| 42 | +print("H2 STO-3G positive control for VQE implementation validation") |
| 43 | +print("Reference FCI ground state energy: ~ -1.1373 Ha") |
| 44 | +print() |
| 45 | + |
| 46 | + |
| 47 | +def geometry_to_atom_str(geometry): |
| 48 | + return "; ".join( |
| 49 | + f"{symbol} {x:.10f} {y:.10f} {z:.10f}" |
| 50 | + for symbol, (x, y, z) in geometry |
| 51 | + ) |
| 52 | + |
| 53 | + |
| 54 | +# ── build Hamiltonians ──────────────────────────────────────────────────────── |
| 55 | + |
| 56 | +molecule_of = MolecularData(geometry, basis, multiplicity=multiplicity, charge=charge) |
| 57 | +molecule_of = run_pyscf(molecule_of, run_scf=True, run_mp2=False, run_cisd=False, |
| 58 | + run_ccsd=False, run_fci=False) |
| 59 | +fermion_ham = get_fermion_operator(molecule_of.get_molecular_hamiltonian()) |
| 60 | +n_electrons = molecule_of.n_electrons |
| 61 | +n_qubits = molecule_of.n_qubits |
| 62 | + |
| 63 | +print(f"Hartree-Fock energy: {molecule_of.hf_energy:.10f} Ha") |
| 64 | +print(f"{n_electrons} electrons, {n_qubits} qubits") |
| 65 | + |
| 66 | +# Interaction graph |
| 67 | +edges = set() |
| 68 | +for term, coeff in fermion_ham.terms.items(): |
| 69 | + jw_term = jordan_wigner(FermionOperator(term=term, coefficient=coeff)) |
| 70 | + for pauli_string, _ in jw_term.terms.items(): |
| 71 | + qubits = [q for q, op in pauli_string if op != 'I'] |
| 72 | + for a, b in itertools.combinations(sorted(qubits), 2): |
| 73 | + edges.add((a, b)) |
| 74 | +ig_edges = sorted(edges) |
| 75 | +n_edges = len(ig_edges) |
| 76 | +print(f"Interaction graph: {n_edges} undirected edges") |
| 77 | + |
| 78 | +# Z-only Hamiltonian for bootstrap energy evaluation |
| 79 | +z_hamiltonian = [] |
| 80 | +for term, coeff in fermion_ham.terms.items(): |
| 81 | + jw_term = jordan_wigner(FermionOperator(term=term, coefficient=coeff)) |
| 82 | + for pauli_string, jw_coeff in jw_term.terms.items(): |
| 83 | + if any(op in ('X', 'Y') for _, op in pauli_string): |
| 84 | + continue |
| 85 | + q = [qubit for qubit, op in pauli_string if op == 'Z'] |
| 86 | + z_hamiltonian.append((q, jw_coeff.real)) |
| 87 | + |
| 88 | +def z_energy(theta): |
| 89 | + energy = 0.0 |
| 90 | + for qubits, coeff in z_hamiltonian: |
| 91 | + c = coeff |
| 92 | + for q in qubits: |
| 93 | + if theta[q]: |
| 94 | + c *= -1 |
| 95 | + energy += c |
| 96 | + return energy |
| 97 | + |
| 98 | +# PennyLane Hamiltonian |
| 99 | +coeffs = [] |
| 100 | +observables = [] |
| 101 | +for term, coeff in fermion_ham.terms.items(): |
| 102 | + jw_term = jordan_wigner(FermionOperator(term=term, coefficient=coeff)) |
| 103 | + for pauli_string, jw_coeff in jw_term.terms.items(): |
| 104 | + ops = [] |
| 105 | + for qubit_idx, pauli in pauli_string: |
| 106 | + if pauli == 'X': ops.append(qml.PauliX(qubit_idx)) |
| 107 | + elif pauli == 'Y': ops.append(qml.PauliY(qubit_idx)) |
| 108 | + elif pauli == 'Z': ops.append(qml.PauliZ(qubit_idx)) |
| 109 | + observables.append(ops[0] if ops else qml.Identity(0)) |
| 110 | + if len(ops) > 1: |
| 111 | + for op in ops[1:]: |
| 112 | + observables[-1] = observables[-1] @ op |
| 113 | + coeffs.append(nppl.array(jw_coeff.real, requires_grad=False)) |
| 114 | +pl_hamiltonian = qml.Hamiltonian(coeffs, observables) |
| 115 | + |
| 116 | + |
| 117 | +# ── VQE runner ──────────────────────────────────────────────────────────────── |
| 118 | + |
| 119 | +def run_vqe(label, bootstrap_theta, n_steps=200, stepsize=np.pi/1800): |
| 120 | + print(f"\n── {label} ──") |
| 121 | + print(f" Initial θ: {bootstrap_theta.astype(int)}") |
| 122 | + initial_energy = z_energy(bootstrap_theta) |
| 123 | + print(f" Initial Z-basis energy: {initial_energy:.10f} Ha") |
| 124 | + |
| 125 | + dev = qml.device("lightning.qubit", wires=n_qubits) |
| 126 | + |
| 127 | + @qml.qnode(dev) |
| 128 | + def circuit(delta, gamma): |
| 129 | + for i in range(n_qubits): |
| 130 | + if bootstrap_theta[i]: |
| 131 | + qml.X(wires=i) |
| 132 | + for i in range(n_qubits): |
| 133 | + qml.RY(delta[i], wires=i) |
| 134 | + for idx, (a, b) in enumerate(ig_edges): |
| 135 | + qml.IsingZZ(gamma[idx], wires=[a, b]) |
| 136 | + return qml.expval(pl_hamiltonian) |
| 137 | + |
| 138 | + delta = nppl.array(np.random.uniform(-0.1, 0.1, n_qubits), requires_grad=True) |
| 139 | + gamma = nppl.array(np.random.uniform(-0.1, 0.1, n_edges), requires_grad=True) |
| 140 | + |
| 141 | + opt = qml.AdamOptimizer(stepsize=stepsize) |
| 142 | + min_energy = initial_energy |
| 143 | + best_delta = delta.copy() |
| 144 | + best_gamma = gamma.copy() |
| 145 | + |
| 146 | + for step in range(n_steps): |
| 147 | + (delta, gamma), energy = opt.step_and_cost(circuit, delta, gamma) |
| 148 | + if float(energy) < min_energy: |
| 149 | + min_energy = float(energy) |
| 150 | + best_delta = delta.copy() |
| 151 | + best_gamma = gamma.copy() |
| 152 | + if (step + 1) % 50 == 0: |
| 153 | + print(f" Step {step+1:3d}: Energy = {float(energy):.10f} Ha") |
| 154 | + |
| 155 | + print(f" Initial energy: {initial_energy:.10f} Ha") |
| 156 | + print(f" Final energy: {min_energy:.10f} Ha") |
| 157 | + print(f" Improvement: {initial_energy - min_energy:.10f} Ha") |
| 158 | + print(f" δ (RY): {np.array(best_delta).round(4)}") |
| 159 | + print(f" γ (RZZ): {np.array(best_gamma).round(4)}") |
| 160 | + |
| 161 | + return min_energy |
| 162 | + |
| 163 | + |
| 164 | +# ── Bootstrap state (correct priors) ───────────────────────────────────────── |
| 165 | +# H2 STO-3G: 2 electrons in 4 spin-orbitals |
| 166 | +# Correct HF state: qubits 0,1 occupied (alpha/beta of bonding orbital) |
| 167 | + |
| 168 | +bootstrap_theta = np.array([True, True, False, False]) |
| 169 | + |
| 170 | +# ── Run three cases ─────────────────────────────────────────────────────────── |
| 171 | + |
| 172 | +e_zeros = run_vqe("Case (a): all-zeros initial state (vacuum)", |
| 173 | + np.array([False, False, False, False])) |
| 174 | + |
| 175 | +e_random = run_vqe("Case (b): random initial state", |
| 176 | + np.array([bool(random.randint(0,1)) for _ in range(n_qubits)])) |
| 177 | + |
| 178 | +e_bootstrap = run_vqe("Case (c): correct bootstrap state", |
| 179 | + bootstrap_theta) |
| 180 | + |
| 181 | +# ── Summary ─────────────────────────────────────────────────────────────────── |
| 182 | + |
| 183 | +print(f"\n{'─'*60}") |
| 184 | +print(f"Hartree-Fock energy: {molecule_of.hf_energy:.10f} Ha") |
| 185 | +print(f"Reference FCI energy: ~ -1.1373000000 Ha") |
| 186 | +print(f"{'─'*60}") |
| 187 | +print(f"Case (a) vacuum → VQE final: {e_zeros:.10f} Ha") |
| 188 | +print(f"Case (b) random → VQE final: {e_random:.10f} Ha") |
| 189 | +print(f"Case (c) bootstrap → VQE final: {e_bootstrap:.10f} Ha") |
| 190 | +print(f"{'─'*60}") |
| 191 | +print() |
| 192 | +print("Interpretation:") |
| 193 | +print(" Cases (a) and (b) should show substantial VQE improvement,") |
| 194 | +print(" demonstrating the optimizer is functional.") |
| 195 | +print(" Case (c) should show little or no improvement,") |
| 196 | +print(" consistent with the bootstrap state being a convex minimum.") |
0 commit comments