Skip to content

Commit b017d06

Browse files
tfim_vqe.py (experimental)
1 parent 1a448bd commit b017d06

File tree

1 file changed

+323
-0
lines changed

1 file changed

+323
-0
lines changed

scripts/tfim_vqe.py

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
# Quantum chemistry example
2+
# Developed with help from (OpenAI custom GPT) Elara
3+
# (Requires PennyLane and OpenFermion)
4+
5+
import pennylane as qml
6+
from pennylane import numpy as np
7+
import openfermion as of
8+
from openfermionpyscf import run_pyscf
9+
from openfermion.transforms import jordan_wigner
10+
11+
from PyQrackIsing import tfim_magnetization
12+
13+
# Step 0: Set environment variables (before running script)
14+
15+
# On command line or by .env file, you can set the following variables
16+
# QRACK_DISABLE_QUNIT_FIDELITY_GUARD=1: For large circuits, automatically "elide," for approximation
17+
# QRACK_NONCLIFFORD_ROUNDING_THRESHOLD=[0 to 1]: Sacrifices near-Clifford accuracy to reduce overhead
18+
# QRACK_QUNIT_SEPARABILITY_THRESHOLD=[0 to 1]: Rounds to separable states more aggressively
19+
# QRACK_QBDT_SEPARABILITY_THRESHOLD=[0 to 0.5]: Rounding for QBDD, if actually used
20+
21+
# Step 1: Define the molecule (Hydrogen, Helium, Lithium, Carbon, Nitrogen, Oxygen)
22+
23+
basis = "sto-3g" # Minimal Basis Set
24+
# basis = '6-31g' # Larger basis set
25+
# basis = 'cc-pVDZ' # Even larger basis set!
26+
multiplicity = 1 # singlet, closed shell, all electrons are paired (neutral molecules with full valence)
27+
# multiplicity = 2 # doublet, one unpaired electron (ex.: OH- radical)
28+
# multiplicity = 3 # triplet, two unpaired electrons (ex.: O2)
29+
charge = 0 # Excess +/- elementary charge, beyond multiplicity
30+
31+
# Hydrogen (and lighter):
32+
33+
geometry = [("H", (0.0, 0.0, 0.0)), ("H", (0.0, 0.0, 0.74))] # H2 Molecule
34+
35+
# Helium (and lighter):
36+
37+
# geometry = [('He', (0.0, 0.0, 0.0)), ('H', (0.0, 0.0, 7.74))] # HeH Molecule
38+
39+
# Lithium (and lighter):
40+
41+
# geometry = [('Li', (0.0, 0.0, 0.0)), ('H', (0.0, 0.0, 15.9))] # LiH Molecule
42+
43+
# Carbon (and lighter):
44+
45+
# Methane (CH4):
46+
# geometry = [
47+
# ('C', (0.0000, 0.0000, 0.0000)), # Central carbon
48+
# ('H', (1.0900, 0.0000, 0.0000)), # Hydrogen 1
49+
# ('H', (-0.3630, 1.0270, 0.0000)), # Hydrogen 2
50+
# ('H', (-0.3630, -0.5130, 0.8890)), # Hydrogen 3
51+
# ('H', (-0.3630, -0.5130, -0.8890)) # Hydrogen 4
52+
# ]
53+
54+
# Nitrogen (and lighter):
55+
56+
# geometry = [('N', (0.0, 0.0, 0.0)), ('N', (0.0, 0.0, 10.9))] # N2 Molecule
57+
58+
# Ammonia:
59+
# geometry = [
60+
# ('N', (0.0000, 0.0000, 0.0000)), # Nitrogen at center
61+
# ('H', (0.9400, 0.0000, -0.3200)), # Hydrogen 1
62+
# ('H', (-0.4700, 0.8130, -0.3200)), # Hydrogen 2
63+
# ('H', (-0.4700, -0.8130, -0.3200)) # Hydrogen 3
64+
# ]
65+
66+
# Oxygen (and lighter):
67+
68+
# geometry = [('O', (0.0, 0.0, 0.0)), ('H', (0.0, 0.0, 9.6))] # OH- Radical
69+
# geometry = [('O', (0.0000, 0.0000, 0.0000)), ('H', (0.7586, 0.0000, 0.5043)), ('H', (-0.7586, 0.0000, 0.5043))] # H2O Molecule
70+
# geometry = [('C', (0.0000, 0.0000, 0.0000)), ('O', (0.0000, 0.0000, 1.128))] # CO Molecule
71+
# geometry = [('C', (0.0000, 0.0000, 0.0000)), ('O', (0.0000, 0.0000, 1.16)), ('O', (0.0000, 0.0000, -1.16))] # CO2 Molecule
72+
# geometry = [('O', (0.0, 0.0, 0.0)), ('N', (0.0, 0.0, 1.55))] # NO Molecule
73+
# geometry = [('O', (0.0, 0.0, 0.0)), ('N', (0.0, 0.0, 11.5))] # NO+ Radical
74+
75+
# Nitrogen dioxide (toxic pollutant)
76+
# geometry = [
77+
# ('N', (0.0000, 0.0000, 0.0000)),
78+
# ('O', (1.2000, 0.0000, 0.0000)),
79+
# ('O', (-1.2000 * np.cos(np.deg2rad(134)), 1.2000 * np.sin(np.deg2rad(134)), 0.0000))
80+
# ]
81+
# geometry = [('N', (0.0000, 0.0000, 0.0000)), ('N', (1.128, 0.0000, 0.0000)), ('O', (2.241, 0.0000, 0.0000))] # N2O Molecule
82+
83+
# Ozone:
84+
# geometry = [
85+
# ('O', (0.0000, 0.0000, 0.0000)), # Central oxygen
86+
# ('O', (1.2780, 0.0000, 0.0000)), # One oxygen
87+
# ('O', (-1.2780 * np.cos(np.deg2rad(117)), 1.2780 * np.sin(np.deg2rad(117)), 0.0000)) # The other oxygen
88+
# ]
89+
90+
# H3O+ (Hydronium Ion):
91+
# bond_length = 10.3 # Scaled bond length (1.03 Å in script units)
92+
# bond_angle = 113.0 # Approximate bond angle of H3O+
93+
# Convert angles to radians
94+
# theta = np.deg2rad(bond_angle)
95+
# Final geometry list
96+
# geometry = [
97+
# ('O', (0.0, 0.0, 0.0)),
98+
# ('H', (bond_length, 0.0, 0.0)),
99+
# ('H', (-bond_length * np.cos(theta), bond_length * np.sin(theta), 0.0)),
100+
# ('H', (-bond_length * np.cos(theta), -bond_length * np.sin(theta), 0.0))
101+
# ]
102+
103+
# Carbonate ion (CO3--):
104+
# geometry = [
105+
# ('C', (0.0000, 0.0000, 0.0000)), # Carbon at center
106+
# ('O', (1.2900, 0.0000, 0.0000)), # Oxygen 1
107+
# ('O', (-0.6450, 1.1180, 0.0000)), # Oxygen 2
108+
# ('O', (-0.6450, -1.1180, 0.0000)) # Oxygen 3
109+
# ]
110+
111+
# Bicarbonate ion (HCO3-):
112+
# geometry = [
113+
# ('C', (0.0000, 0.0000, 0.0000)), # Carbon center
114+
# ('O', (1.2200, 0.0000, 0.0000)), # Oxygen (C=O)
115+
# ('O', (-0.6100, 1.0550, 0.0000)), # Oxygen (-O⁻)
116+
# ('O', (-0.6100, -1.0550, 0.0000)), # Oxygen (OH)
117+
# ('H', (-1.2200, -1.0550, 0.0000)) # Hydrogen in OH group
118+
# ]
119+
120+
# Sulfur chemistry:
121+
122+
# Hydrogen sulfide (rotten egg smell, major biologic sulfur compound):
123+
# geometry = [
124+
# ('S', (0.0000, 0.0000, 0.0000)),
125+
# ('H', (1.3400, 0.0000, 0.0000)),
126+
# ('H', (-1.3400 * np.cos(np.deg2rad(92)), 1.3400 * np.sin(np.deg2rad(92)), 0.0000))
127+
# ]
128+
129+
# Sulfur dioxide (major volcanic gas and pollutant):
130+
# geometry = [
131+
# ('S', (0.0000, 0.0000, 0.0000)), # Sulfur at center
132+
# ('O', (1.4300, 0.0000, 0.0000)), # Oxygen 1
133+
# ('O', (-1.4300 * np.cos(np.deg2rad(119)), 1.4300 * np.sin(np.deg2rad(119)), 0.0000)) # Oxygen 2
134+
# ]
135+
136+
# Sulfur trioxide (key in acid rain, forms H₂SO₄):
137+
# geometry = [
138+
# ('S', (0.0000, 0.0000, 0.0000)),
139+
# ('O', (1.4200, 0.0000, 0.0000)),
140+
# ('O', (-0.7100, 1.2290, 0.0000)),
141+
# ('O', (-0.7100, -1.2290, 0.0000))
142+
# ]
143+
144+
# Sulfate ion (SO4--, major oceanic anion, ionically bonds to Mg++):
145+
# geometry = [
146+
# ('S', (0.0000, 0.0000, 0.0000)),
147+
# ('O', (1.4900, 0.0000, 0.0000)),
148+
# ('O', (-0.7450, 1.2900, 0.0000)),
149+
# ('O', (-0.7450, -1.2900, 0.0000)),
150+
# ('O', (0.0000, 0.0000, 1.4900))
151+
# ]
152+
153+
# Oceanic electrolytes (consider isolating cations and anions as single atoms with excess charge):
154+
155+
# Sodium chloride:
156+
# geometry = [
157+
# ('Na', (0.0000, 0.0000, 0.0000)),
158+
# ('Cl', (2.3600, 0.0000, 0.0000))
159+
# ]
160+
161+
# Potassium chloride (biologically important):
162+
# geometry = [
163+
# ('K', (0.0000, 0.0000, 0.0000)),
164+
# ('Cl', (2.6700, 0.0000, 0.0000))
165+
# ]
166+
167+
# Calcium chloride:
168+
# geometry = [
169+
# ('Ca', (0.0000, 0.0000, 0.0000)),
170+
# ('Cl', (2.7800, 0.0000, 0.0000)),
171+
# ('Cl', (-2.7800, 0.0000, 0.0000))
172+
# ]
173+
174+
# Diatomic halogens:
175+
176+
# Fluorine gas (F2)
177+
# geometry = [('F', (0.0000, 0.0000, 0.0000)), ('F', (1.4200, 0.0000, 0.0000))]
178+
179+
# Chlorine gas (Cl2)
180+
# geometry = [('Cl', (0.0000, 0.0000, 0.0000)), ('Cl', (1.9900, 0.0000, 0.0000))]
181+
182+
# Silicon dioxide (quartz, sand, granite):
183+
# geometry = [
184+
# ('Si', (0.0000, 0.0000, 0.0000)), # Silicon at center
185+
# ('O', (1.6200, 0.0000, 0.0000)), # Oxygen 1
186+
# ('O', (-1.6200, 0.0000, 0.0000)) # Oxygen 2
187+
# ]
188+
189+
# The above are major atmospheric, oceanic, and soil components on Earth.
190+
# Proper organic chemistry is beyond the scope of this script,
191+
# but we give a memorable token example of a carbon ring.
192+
193+
# Benzene (C6H6)
194+
195+
# Define bond lengths (in angstroms, converted to script units)
196+
# C_C = 13.9 # Carbon-carbon bond (1.39 Å)
197+
# C_H = 10.9 # Carbon-hydrogen bond (1.09 Å)
198+
199+
# Angle of 120° between C-C bonds in the hexagonal ring
200+
# theta = np.deg2rad(120)
201+
202+
# Define carbon positions (hexagonal ring)
203+
# geometry = [
204+
# ('C', (C_C, 0.0, 0.0)), # First carbon at x-axis
205+
# ('C', (C_C * np.cos(theta), C_C * np.sin(theta), 0.0)),
206+
# ('C', (-C_C * np.cos(theta), C_C * np.sin(theta), 0.0)),
207+
# ('C', (-C_C, 0.0, 0.0)),
208+
# ('C', (-C_C * np.cos(theta), -C_C * np.sin(theta), 0.0)),
209+
# ('C', (C_C * np.cos(theta), -C_C * np.sin(theta), 0.0))
210+
# ]
211+
212+
# Define hydrogen positions (bonded to carbons)
213+
# for i in range(6):
214+
# x, y, z = geometry[i][1] # Get carbon position
215+
# hydrogen_x = x + (C_H * (x / C_C)) # Extend outward along C-C axis
216+
# hydrogen_y = y + (C_H * (y / C_C))
217+
# hydrogen_z = z # Planar
218+
# geometry.append(('H', (hydrogen_x, hydrogen_y, hydrogen_z)))
219+
220+
# Now, `geometry` contains all 6 carbons and 6 hydrogens!
221+
222+
# Step 2: Compute the Molecular Hamiltonian
223+
molecule = of.MolecularData(geometry, basis, multiplicity, charge)
224+
molecule = run_pyscf(molecule, run_scf=True, run_fci=True)
225+
fermionic_hamiltonian = molecule.get_molecular_hamiltonian()
226+
n_qubits = molecule.n_qubits # Auto-detect qubit count
227+
print(str(n_qubits) + " qubits...")
228+
229+
# Step 3: Convert to Qubit Hamiltonian (Jordan-Wigner)
230+
qubit_hamiltonian = jordan_wigner(fermionic_hamiltonian)
231+
232+
# Step 4: Setup localized TFIM from Hamiltonian
233+
234+
def estimate_local_parameters(qubit_hamiltonian, n_qubits):
235+
"""
236+
Estimate (z, J, h) per qubit from Hamiltonian.
237+
z: number of ZZ neighbors
238+
J: average ZZ coefficient
239+
h: average X coefficient
240+
"""
241+
z = np.zeros(n_qubits, dtype=int)
242+
J = np.zeros(n_qubits)
243+
h = np.zeros(n_qubits)
244+
245+
for term, coeff in qubit_hamiltonian.terms.items():
246+
if len(term) == 1:
247+
q, pauli = term[0]
248+
if pauli == "X":
249+
h[q] += np.abs(coeff)
250+
elif len(term) == 2:
251+
(q1, p1), (q2, p2) = term
252+
if {p1, p2} == {"Z"}:
253+
J[q1] += np.abs(coeff) / 2
254+
J[q2] += np.abs(coeff) / 2
255+
z[q1] += 1
256+
z[q2] += 1
257+
return z, J, h
258+
259+
def tfim_ground_state_angles(n_qubits, J_vec, h_vec, z_vec, t=10.0):
260+
"""
261+
Generate RY angles based on TFIM magnetization estimates.
262+
"""
263+
ry_angles = np.zeros(n_qubits)
264+
for i in range(n_qubits):
265+
m = tfim_magnetization(J=J_vec[i], h=h_vec[i], z=z_vec[i], theta=np.random.rand() * np.pi, t=t, n_qubits=1)
266+
p = np.clip((1.0 - m) / 2.0, 1e-6, 1 - 1e-6)
267+
ry_angles[i] = 2.0 * np.arcsin(np.sqrt(p))
268+
return ry_angles
269+
270+
def build_ansatz(angles, wires):
271+
for i, angle in enumerate(angles):
272+
qml.RY(angle, wires=wires[i])
273+
for i in range(len(wires) - 1):
274+
qml.CNOT(wires=[wires[i], wires[i+1]])
275+
276+
def tfim_energy_estimate(qubit_hamiltonian, n_qubits, dev=None):
277+
"""
278+
Estimate energy from TFIM-predicted RY angles.
279+
"""
280+
z, J, h = estimate_local_parameters(qubit_hamiltonian, n_qubits)
281+
angles = tfim_ground_state_angles(n_qubits, J, h, z)
282+
283+
if dev is None:
284+
dev = qml.device("default.qubit", wires=n_qubits)
285+
286+
# Step 4: Extract Qubit Terms for PennyLane
287+
coeffs = []
288+
observables = []
289+
n_qubits = molecule.n_qubits # Auto-detect qubit count
290+
print(str(n_qubits) + " qubits...")
291+
292+
for term, coefficient in qubit_hamiltonian.terms.items():
293+
pauli_operators = []
294+
for qubit_idx, pauli in term:
295+
if pauli == "X":
296+
pauli_operators.append(qml.PauliX(qubit_idx))
297+
elif pauli == "Y":
298+
pauli_operators.append(qml.PauliY(qubit_idx))
299+
elif pauli == "Z":
300+
pauli_operators.append(qml.PauliZ(qubit_idx))
301+
if pauli_operators:
302+
observable = pauli_operators[0]
303+
for op in pauli_operators[1:]:
304+
observable = observable @ op
305+
observables.append(observable)
306+
else:
307+
observables.append(qml.Identity(0)) # Default identity if no operators
308+
coeffs.append(qml.numpy.array(coefficient, requires_grad=False))
309+
310+
hamiltonian = qml.Hamiltonian(coeffs, observables)
311+
312+
@qml.qnode(dev)
313+
def circuit():
314+
build_ansatz(angles, wires=range(n_qubits))
315+
return qml.expval(hamiltonian)
316+
317+
return circuit()
318+
319+
# Step 5: Setup Qrack simulator and calculate energy expectation value
320+
dev = qml.device("qrack.simulator", wires=n_qubits)
321+
energy = tfim_energy_estimate(qubit_hamiltonian, n_qubits, dev)
322+
323+
print(f"Optimized Ground State Energy: {energy} Ha")

0 commit comments

Comments
 (0)