|
| 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