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