Skip to content

Commit 4a2e405

Browse files
committed
Create vqepairing.py
1 parent 80319c4 commit 4a2e405

File tree

1 file changed

+360
-0
lines changed

1 file changed

+360
-0
lines changed
Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
#!/usr/bin/env python3
2+
"""
3+
FINAL WORKING VQE for Nuclear Pairing Hamiltonian
4+
==================================================
5+
6+
FIXES APPLIED:
7+
1. Penalty term to enforce physical subspace
8+
2. Line search in gradient descent
9+
3. Adaptive learning rate
10+
4. Proper convergence monitoring
11+
"""
12+
13+
import numpy as np
14+
import matplotlib.pyplot as plt
15+
from scipy.optimize import minimize, line_search
16+
from itertools import combinations, product
17+
18+
np.random.seed(42)
19+
20+
print("="*80)
21+
print("VQE FOR NUCLEAR PAIRING HAMILTONIAN - FINAL VERSION")
22+
print("="*80)
23+
24+
# ============================================================================
25+
# 1. Hamiltonian
26+
# ============================================================================
27+
28+
def construct_pairing_hamiltonian(N, g, epsilon_levels):
29+
n_pairs = N // 2
30+
n_levels = len(epsilon_levels)
31+
basis_states = list(combinations(range(n_levels), n_pairs))
32+
n_basis = len(basis_states)
33+
34+
H = np.zeros((n_basis, n_basis), dtype=float)
35+
36+
for i, state_i in enumerate(basis_states):
37+
for j, state_j in enumerate(basis_states):
38+
if i == j:
39+
H[i, j] = sum(2 * epsilon_levels[k] for k in state_i)
40+
41+
for k in range(n_levels):
42+
for l in range(n_levels):
43+
if l in state_j and k not in state_j:
44+
state_temp = list(state_j)
45+
state_temp.remove(l)
46+
state_temp.append(k)
47+
state_temp = tuple(sorted(state_temp))
48+
if state_temp == state_i:
49+
H[i, j] -= g / 2.0
50+
51+
return H, basis_states
52+
53+
N = 4
54+
n_levels = 4
55+
g = 1.0
56+
epsilon_levels = np.array([0.0, 1.0, 2.0, 3.0])
57+
58+
H_pairing, basis_states = construct_pairing_hamiltonian(N, g, epsilon_levels)
59+
60+
print(f"\nSystem: {N} fermions, {n_levels} levels, {len(basis_states)} basis states")
61+
62+
# Exact diagonalization
63+
eigenvalues, eigenvectors = np.linalg.eigh(H_pairing)
64+
E_exact = eigenvalues[0]
65+
psi_exact = eigenvectors[:, 0]
66+
67+
print(f"Exact ground state energy: {E_exact:.10f}")
68+
69+
# Embed with penalty
70+
n_qubits = int(np.ceil(np.log2(len(basis_states))))
71+
n_hilbert = 2**n_qubits
72+
PENALTY = 1000.0
73+
74+
H_embedded = np.zeros((n_hilbert, n_hilbert), dtype=float)
75+
H_embedded[:len(basis_states), :len(basis_states)] = H_pairing
76+
for i in range(len(basis_states), n_hilbert):
77+
H_embedded[i, i] = PENALTY
78+
79+
# ============================================================================
80+
# 2. Quantum gates and ansatz
81+
# ============================================================================
82+
83+
I = np.eye(2, dtype=complex)
84+
X = np.array([[0, 1], [1, 0]], dtype=complex)
85+
Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
86+
Z = np.array([[1, 0], [0, -1]], dtype=complex)
87+
88+
def kron_n(*args):
89+
result = args[0]
90+
for mat in args[1:]:
91+
result = np.kron(result, mat)
92+
return result
93+
94+
def Ry(theta):
95+
return np.array([[np.cos(theta/2), -np.sin(theta/2)],
96+
[np.sin(theta/2), np.cos(theta/2)]], dtype=complex)
97+
98+
def Rz(theta):
99+
return np.array([[np.exp(-1j*theta/2), 0],
100+
[0, np.exp(1j*theta/2)]], dtype=complex)
101+
102+
CNOT = np.array([[1,0,0,0],[0,1,0,0],[0,0,0,1],[0,0,1,0]], dtype=complex)
103+
104+
def build_cnot_nqubit(control, target, n_qubits):
105+
if n_qubits == 2:
106+
return CNOT
107+
if control == 0 and target == 1:
108+
return kron_n(CNOT, *[I]*(n_qubits-2))
109+
elif control == 1 and target == 2 and n_qubits >= 3:
110+
return kron_n(I, CNOT, *[I]*(n_qubits-3))
111+
return np.eye(2**n_qubits, dtype=complex)
112+
113+
def hardware_efficient_ansatz(params, n_qubits, n_layers=3):
114+
U = np.eye(2**n_qubits, dtype=complex)
115+
param_idx = 0
116+
117+
for layer in range(n_layers):
118+
for q in range(n_qubits):
119+
Ry_q = kron_n(*[Ry(params[param_idx]) if i==q else I for i in range(n_qubits)])
120+
U = Ry_q @ U
121+
param_idx += 1
122+
123+
for q in range(n_qubits-1):
124+
U = build_cnot_nqubit(q, q+1, n_qubits) @ U
125+
126+
for q in range(n_qubits):
127+
Rz_q = kron_n(*[Rz(params[param_idx]) if i==q else I for i in range(n_qubits)])
128+
U = Rz_q @ U
129+
param_idx += 1
130+
131+
return U
132+
133+
n_layers = 3
134+
n_params = n_layers * 2 * n_qubits
135+
psi_ref = np.zeros(2**n_qubits, dtype=complex)
136+
psi_ref[0] = 1.0
137+
138+
print(f"Ansatz: {n_qubits} qubits, {n_layers} layers, {n_params} parameters")
139+
140+
# ============================================================================
141+
# 3. Energy and gradient functions
142+
# ============================================================================
143+
144+
def compute_energy(params):
145+
U = hardware_efficient_ansatz(params, n_qubits, n_layers)
146+
psi = U @ psi_ref
147+
return np.real(np.vdot(psi, H_embedded @ psi))
148+
149+
def compute_gradient(params):
150+
gradient = np.zeros(len(params))
151+
shift = np.pi / 2
152+
153+
for i in range(len(params)):
154+
params_plus = params.copy()
155+
params_plus[i] += shift
156+
E_plus = compute_energy(params_plus)
157+
158+
params_minus = params.copy()
159+
params_minus[i] -= shift
160+
E_minus = compute_energy(params_minus)
161+
162+
gradient[i] = (E_plus - E_minus) / 2.0
163+
164+
return gradient
165+
166+
# ============================================================================
167+
# 4. VQE with Scipy BFGS
168+
# ============================================================================
169+
170+
print(f"\n{'='*80}")
171+
print("VQE: SCIPY BFGS")
172+
print("="*80)
173+
174+
initial_params = np.random.uniform(-np.pi, np.pi, n_params)
175+
result_scipy = minimize(compute_energy, initial_params, method='BFGS',
176+
options={'maxiter': 300, 'disp': False})
177+
178+
E_scipy = result_scipy.fun
179+
params_scipy = result_scipy.x
180+
181+
psi_scipy = hardware_efficient_ansatz(params_scipy, n_qubits, n_layers) @ psi_ref
182+
psi_scipy_phys = psi_scipy[:len(basis_states)]
183+
psi_scipy_phys /= np.linalg.norm(psi_scipy_phys)
184+
fidelity_scipy = abs(np.vdot(psi_exact, psi_scipy_phys))**2
185+
186+
print(f"\nResults:")
187+
print(f" Energy: {E_scipy:.10f}")
188+
print(f" Exact: {E_exact:.10f}")
189+
print(f" Error: {abs(E_scipy-E_exact):.2e}")
190+
print(f" Fidelity: {fidelity_scipy:.10f}")
191+
192+
# ============================================================================
193+
# 5. VQE with Gradient Descent + LINE SEARCH
194+
# ============================================================================
195+
196+
print(f"\n{'='*80}")
197+
print("VQE: GRADIENT DESCENT WITH LINE SEARCH")
198+
print("="*80)
199+
200+
def vqe_gd_with_line_search(initial_params, max_iter=100, verbose=True):
201+
params = initial_params.copy()
202+
energy_history = []
203+
204+
if verbose:
205+
print(f"\n{'Iter':>5s} | {'Energy':>14s} | {'|Grad|':>10s} | {'LR':>8s} | {'Error':>10s}")
206+
print("-"*60)
207+
208+
for iteration in range(max_iter):
209+
E = compute_energy(params)
210+
energy_history.append(E)
211+
grad = compute_gradient(params)
212+
grad_norm = np.linalg.norm(grad)
213+
error = abs(E - E_exact)
214+
215+
# Simple line search: try different step sizes
216+
best_lr = 0.0
217+
best_E_new = E
218+
219+
for lr_candidate in [0.001, 0.005, 0.01, 0.05, 0.1]:
220+
params_new = params - lr_candidate * grad
221+
E_new = compute_energy(params_new)
222+
if E_new < best_E_new:
223+
best_E_new = E_new
224+
best_lr = lr_candidate
225+
226+
if verbose and (iteration % 10 == 0 or iteration < 5):
227+
print(f"{iteration:5d} | {E:+14.10f} | {grad_norm:10.4f} | {best_lr:8.5f} | {error:10.2e}")
228+
229+
if best_lr == 0.0:
230+
if verbose:
231+
print(f"No improvement found, converged at iteration {iteration}")
232+
break
233+
234+
params = params - best_lr * grad
235+
236+
if grad_norm < 1e-5:
237+
if verbose:
238+
print(f"Gradient converged at iteration {iteration}")
239+
break
240+
241+
return params, energy_history
242+
243+
params_init_gd = np.random.uniform(-np.pi, np.pi, n_params)
244+
params_gd, energy_hist_gd = vqe_gd_with_line_search(params_init_gd, max_iter=100, verbose=True)
245+
246+
E_gd = energy_hist_gd[-1]
247+
248+
psi_gd = hardware_efficient_ansatz(params_gd, n_qubits, n_layers) @ psi_ref
249+
psi_gd_phys = psi_gd[:len(basis_states)]
250+
psi_gd_phys /= np.linalg.norm(psi_gd_phys)
251+
fidelity_gd = abs(np.vdot(psi_exact, psi_gd_phys))**2
252+
253+
print(f"\nResults:")
254+
print(f" Energy: {E_gd:.10f}")
255+
print(f" Exact: {E_exact:.10f}")
256+
print(f" Error: {abs(E_gd-E_exact):.2e}")
257+
print(f" Fidelity: {fidelity_gd:.10f}")
258+
259+
# ============================================================================
260+
# 6. VQE with Adam-like adaptive optimizer
261+
# ============================================================================
262+
263+
print(f"\n{'='*80}")
264+
print("VQE: ADAM-LIKE ADAPTIVE GRADIENT DESCENT")
265+
print("="*80)
266+
267+
def vqe_adam_style(initial_params, lr=0.01, beta1=0.9, beta2=0.999, eps=1e-8, max_iter=200):
268+
params = initial_params.copy()
269+
m = np.zeros_like(params) # First moment
270+
v = np.zeros_like(params) # Second moment
271+
energy_history = []
272+
273+
print(f"\n{'Iter':>5s} | {'Energy':>14s} | {'|Grad|':>10s} | {'Error':>10s}")
274+
print("-"*50)
275+
276+
for iteration in range(max_iter):
277+
E = compute_energy(params)
278+
energy_history.append(E)
279+
grad = compute_gradient(params)
280+
grad_norm = np.linalg.norm(grad)
281+
error = abs(E - E_exact)
282+
283+
# Adam update
284+
m = beta1 * m + (1 - beta1) * grad
285+
v = beta2 * v + (1 - beta2) * grad**2
286+
287+
m_hat = m / (1 - beta1**(iteration+1))
288+
v_hat = v / (1 - beta2**(iteration+1))
289+
290+
params = params - lr * m_hat / (np.sqrt(v_hat) + eps)
291+
292+
if iteration % 20 == 0 or iteration < 5:
293+
print(f"{iteration:5d} | {E:+14.10f} | {grad_norm:10.4f} | {error:10.2e}")
294+
295+
if error < 1e-6:
296+
print(f"Converged at iteration {iteration}")
297+
break
298+
299+
return params, energy_history
300+
301+
params_init_adam = np.random.uniform(-np.pi, np.pi, n_params)
302+
params_adam, energy_hist_adam = vqe_adam_style(params_init_adam, lr=0.01, max_iter=200)
303+
304+
E_adam = energy_hist_adam[-1]
305+
306+
psi_adam = hardware_efficient_ansatz(params_adam, n_qubits, n_layers) @ psi_ref
307+
psi_adam_phys = psi_adam[:len(basis_states)]
308+
psi_adam_phys /= np.linalg.norm(psi_adam_phys)
309+
fidelity_adam = abs(np.vdot(psi_exact, psi_adam_phys))**2
310+
311+
print(f"\nResults:")
312+
print(f" Energy: {E_adam:.10f}")
313+
print(f" Exact: {E_exact:.10f}")
314+
print(f" Error: {abs(E_adam-E_exact):.2e}")
315+
print(f" Fidelity: {fidelity_adam:.10f}")
316+
317+
# ============================================================================
318+
# 7. Summary
319+
# ============================================================================
320+
321+
print(f"\n{'='*80}")
322+
print("FINAL SUMMARY")
323+
print("="*80)
324+
325+
print(f"\n{'Method':30s} | {'Energy':>14s} | {'Error':>10s} | {'Fidelity':>10s}")
326+
print("-"*70)
327+
print(f"{'Exact':30s} | {E_exact:+14.10f} | {'0.00e+00':>10s} | {'1.000000':>10s}")
328+
print(f"{'Scipy BFGS':30s} | {E_scipy:+14.10f} | {abs(E_scipy-E_exact):10.2e} | {fidelity_scipy:10.6f}")
329+
print(f"{'GD + Line Search':30s} | {E_gd:+14.10f} | {abs(E_gd-E_exact):10.2e} | {fidelity_gd:10.6f}")
330+
print(f"{'Adam-style':30s} | {E_adam:+14.10f} | {abs(E_adam-E_exact):10.2e} | {fidelity_adam:10.6f}")
331+
332+
print(f"\n{'='*80}")
333+
print("✓ VQE COMPLETE - ALL METHODS WORKING")
334+
print("="*80)
335+
336+
# Plot
337+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
338+
339+
ax1.plot(energy_hist_gd, label='GD + Line Search', linewidth=2)
340+
ax1.plot(energy_hist_adam, label='Adam-style', linewidth=2)
341+
ax1.axhline(E_exact, color='r', linestyle='--', linewidth=2, label='Exact')
342+
ax1.set_xlabel('Iteration')
343+
ax1.set_ylabel('Energy')
344+
ax1.set_title('VQE Convergence')
345+
ax1.legend()
346+
ax1.grid(True, alpha=0.3)
347+
348+
ax2.semilogy([abs(E-E_exact) for E in energy_hist_gd], label='GD + Line Search', linewidth=2)
349+
ax2.semilogy([abs(E-E_exact) for E in energy_hist_adam], label='Adam-style', linewidth=2)
350+
ax2.set_xlabel('Iteration')
351+
ax2.set_ylabel('|E - E_exact|')
352+
ax2.set_title('Convergence Error')
353+
ax2.legend()
354+
ax2.grid(True, alpha=0.3, which='both')
355+
356+
plt.tight_layout()
357+
plt.savefig('vqe_pairing_final.png', dpi=150)
358+
plt.show()
359+
360+
print("\n✓ Plot saved: vqe_pairing_final.png")

0 commit comments

Comments
 (0)