Initial commit for pennylane
This commit is contained in:
567
references/quantum_chemistry.md
Normal file
567
references/quantum_chemistry.md
Normal file
@@ -0,0 +1,567 @@
|
||||
# Quantum Chemistry with PennyLane
|
||||
|
||||
## Table of Contents
|
||||
1. [Molecular Hamiltonians](#molecular-hamiltonians)
|
||||
2. [Variational Quantum Eigensolver (VQE)](#variational-quantum-eigensolver-vqe)
|
||||
3. [Molecular Structure](#molecular-structure)
|
||||
4. [Basis Sets and Mapping](#basis-sets-and-mapping)
|
||||
5. [Excited States](#excited-states)
|
||||
6. [Quantum Chemistry Workflows](#quantum-chemistry-workflows)
|
||||
|
||||
## Molecular Hamiltonians
|
||||
|
||||
### Building Molecular Hamiltonians
|
||||
|
||||
```python
|
||||
import pennylane as qml
|
||||
from pennylane import qchem
|
||||
import numpy as np
|
||||
|
||||
# Define molecule
|
||||
symbols = ['H', 'H']
|
||||
coordinates = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.74]) # Angstroms
|
||||
|
||||
# Generate Hamiltonian
|
||||
hamiltonian, n_qubits = qchem.molecular_hamiltonian(
|
||||
symbols,
|
||||
coordinates,
|
||||
charge=0,
|
||||
mult=1, # Spin multiplicity
|
||||
basis='sto-3g',
|
||||
method='dhf' # Dirac-Hartree-Fock
|
||||
)
|
||||
|
||||
print(f"Hamiltonian: {hamiltonian}")
|
||||
print(f"Number of qubits needed: {n_qubits}")
|
||||
```
|
||||
|
||||
### Jordan-Wigner Transformation
|
||||
|
||||
```python
|
||||
# Hamiltonian is automatically in qubit form via Jordan-Wigner
|
||||
# Manual transformation:
|
||||
from pennylane import fermi
|
||||
|
||||
# Fermionic operators
|
||||
a_0 = fermi.FermiC(0) # Creation operator
|
||||
a_1 = fermi.FermiA(1) # Annihilation operator
|
||||
|
||||
# Convert to qubits
|
||||
qubit_op = qml.qchem.jordan_wigner(a_0 * a_1)
|
||||
```
|
||||
|
||||
### Bravyi-Kitaev Transformation
|
||||
|
||||
```python
|
||||
# Alternative mapping (more efficient for some systems)
|
||||
from pennylane.qchem import bravyi_kitaev
|
||||
|
||||
# Build Hamiltonian with Bravyi-Kitaev
|
||||
hamiltonian, n_qubits = qchem.molecular_hamiltonian(
|
||||
symbols,
|
||||
coordinates,
|
||||
mapping='bravyi_kitaev'
|
||||
)
|
||||
```
|
||||
|
||||
### Custom Hamiltonians
|
||||
|
||||
```python
|
||||
# Build Hamiltonian from coefficients and operators
|
||||
coeffs = [0.2, -0.8, 0.5]
|
||||
obs = [
|
||||
qml.PauliZ(0),
|
||||
qml.PauliZ(0) @ qml.PauliZ(1),
|
||||
qml.PauliX(0) @ qml.PauliX(1)
|
||||
]
|
||||
|
||||
H = qml.Hamiltonian(coeffs, obs)
|
||||
|
||||
# Or use simplified syntax
|
||||
H = 0.2 * qml.PauliZ(0) - 0.8 * qml.PauliZ(0) @ qml.PauliZ(1) + 0.5 * qml.PauliX(0) @ qml.PauliX(1)
|
||||
```
|
||||
|
||||
## Variational Quantum Eigensolver (VQE)
|
||||
|
||||
### Basic VQE Implementation
|
||||
|
||||
```python
|
||||
from pennylane import numpy as np
|
||||
|
||||
# Define device
|
||||
dev = qml.device('default.qubit', wires=n_qubits)
|
||||
|
||||
# Hartree-Fock state preparation
|
||||
hf_state = qchem.hf_state(electrons=2, orbitals=n_qubits)
|
||||
|
||||
def ansatz(params, wires):
|
||||
"""Variational ansatz."""
|
||||
qml.BasisState(hf_state, wires=wires)
|
||||
|
||||
for i in range(len(wires)):
|
||||
qml.RY(params[i], wires=i)
|
||||
|
||||
for i in range(len(wires)-1):
|
||||
qml.CNOT(wires=[i, i+1])
|
||||
|
||||
@qml.qnode(dev)
|
||||
def vqe_circuit(params):
|
||||
ansatz(params, wires=range(n_qubits))
|
||||
return qml.expval(hamiltonian)
|
||||
|
||||
# Optimize
|
||||
opt = qml.GradientDescentOptimizer(stepsize=0.4)
|
||||
params = np.random.normal(0, np.pi, n_qubits, requires_grad=True)
|
||||
|
||||
for n in range(100):
|
||||
params, energy = opt.step_and_cost(vqe_circuit, params)
|
||||
|
||||
if n % 20 == 0:
|
||||
print(f"Step {n}: Energy = {energy:.8f} Ha")
|
||||
|
||||
print(f"Final ground state energy: {energy:.8f} Ha")
|
||||
```
|
||||
|
||||
### UCCSD Ansatz
|
||||
|
||||
```python
|
||||
from pennylane.qchem import UCCSD
|
||||
|
||||
# Singles and doubles excitations
|
||||
singles, doubles = qchem.excitations(electrons=2, orbitals=n_qubits)
|
||||
|
||||
@qml.qnode(dev)
|
||||
def uccsd_circuit(params):
|
||||
# Hartree-Fock reference
|
||||
qml.BasisState(hf_state, wires=range(n_qubits))
|
||||
|
||||
# UCCSD ansatz
|
||||
UCCSD(params, wires=range(n_qubits), s_wires=singles, d_wires=doubles)
|
||||
|
||||
return qml.expval(hamiltonian)
|
||||
|
||||
# Initialize parameters
|
||||
n_params = len(singles) + len(doubles)
|
||||
params = np.zeros(n_params, requires_grad=True)
|
||||
|
||||
# Optimize
|
||||
opt = qml.AdamOptimizer(stepsize=0.1)
|
||||
for n in range(100):
|
||||
params, energy = opt.step_and_cost(uccsd_circuit, params)
|
||||
```
|
||||
|
||||
### Adaptive VQE
|
||||
|
||||
```python
|
||||
def adaptive_vqe(hamiltonian, n_qubits, max_gates=10):
|
||||
"""Adaptive VQE: Grow ansatz iteratively."""
|
||||
dev = qml.device('default.qubit', wires=n_qubits)
|
||||
|
||||
# Start with HF state
|
||||
operations = []
|
||||
params = []
|
||||
|
||||
hf_state = qchem.hf_state(electrons=2, orbitals=n_qubits)
|
||||
|
||||
@qml.qnode(dev)
|
||||
def circuit(p):
|
||||
qml.BasisState(hf_state, wires=range(n_qubits))
|
||||
|
||||
for op, param in zip(operations, p):
|
||||
op(param)
|
||||
|
||||
return qml.expval(hamiltonian)
|
||||
|
||||
# Iteratively add gates
|
||||
for _ in range(max_gates):
|
||||
# Find best gate to add
|
||||
best_op = None
|
||||
best_improvement = 0
|
||||
|
||||
for candidate_op in generate_candidates():
|
||||
# Test adding this operation
|
||||
test_ops = operations + [candidate_op]
|
||||
test_params = params + [0.0]
|
||||
|
||||
improvement = evaluate_improvement(test_ops, test_params)
|
||||
|
||||
if improvement > best_improvement:
|
||||
best_improvement = improvement
|
||||
best_op = candidate_op
|
||||
|
||||
if best_improvement < threshold:
|
||||
break
|
||||
|
||||
operations.append(best_op)
|
||||
params.append(0.0)
|
||||
|
||||
# Optimize current ansatz
|
||||
opt = qml.AdamOptimizer(stepsize=0.1)
|
||||
for _ in range(50):
|
||||
params = opt.step(circuit, params)
|
||||
|
||||
return circuit, params
|
||||
```
|
||||
|
||||
## Molecular Structure
|
||||
|
||||
### Defining Molecules
|
||||
|
||||
```python
|
||||
# Simple diatomic
|
||||
h2_symbols = ['H', 'H']
|
||||
h2_coords = np.array([0.0, 0.0, 0.0, 0.0, 0.0, 0.74])
|
||||
|
||||
# Water molecule
|
||||
h2o_symbols = ['O', 'H', 'H']
|
||||
h2o_coords = np.array([
|
||||
0.0, 0.0, 0.0, # O
|
||||
0.757, 0.586, 0.0, # H
|
||||
-0.757, 0.586, 0.0 # H
|
||||
])
|
||||
|
||||
# From XYZ format
|
||||
molecule = qchem.read_structure('molecule.xyz')
|
||||
symbols, coords = molecule
|
||||
```
|
||||
|
||||
### Geometry Optimization
|
||||
|
||||
```python
|
||||
def optimize_geometry(symbols, initial_coords, basis='sto-3g'):
|
||||
"""Optimize molecular geometry."""
|
||||
|
||||
def energy_surface(coords):
|
||||
H, n_qubits = qchem.molecular_hamiltonian(
|
||||
symbols, coords, basis=basis
|
||||
)
|
||||
|
||||
# Run VQE to get energy
|
||||
energy = run_vqe(H, n_qubits)
|
||||
return energy
|
||||
|
||||
# Classical optimization of nuclear coordinates
|
||||
from scipy.optimize import minimize
|
||||
|
||||
result = minimize(
|
||||
energy_surface,
|
||||
initial_coords,
|
||||
method='BFGS',
|
||||
options={'gtol': 1e-5}
|
||||
)
|
||||
|
||||
return result.x, result.fun
|
||||
|
||||
optimized_coords, min_energy = optimize_geometry(h2_symbols, h2_coords)
|
||||
print(f"Optimized geometry: {optimized_coords}")
|
||||
print(f"Energy: {min_energy} Ha")
|
||||
```
|
||||
|
||||
### Bond Dissociation Curves
|
||||
|
||||
```python
|
||||
def dissociation_curve(symbols, axis=2, distances=None):
|
||||
"""Calculate potential energy surface."""
|
||||
|
||||
if distances is None:
|
||||
distances = np.linspace(0.5, 3.0, 20)
|
||||
|
||||
energies = []
|
||||
|
||||
for d in distances:
|
||||
coords = np.zeros(6)
|
||||
coords[axis] = d # Set bond length
|
||||
|
||||
H, n_qubits = qchem.molecular_hamiltonian(
|
||||
symbols, coords, basis='sto-3g'
|
||||
)
|
||||
|
||||
energy = run_vqe(H, n_qubits)
|
||||
energies.append(energy)
|
||||
|
||||
print(f"Distance: {d:.2f} Å, Energy: {energy:.6f} Ha")
|
||||
|
||||
return distances, energies
|
||||
|
||||
# H2 dissociation
|
||||
distances, energies = dissociation_curve(['H', 'H'])
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
plt.plot(distances, energies)
|
||||
plt.xlabel('Bond length (Å)')
|
||||
plt.ylabel('Energy (Ha)')
|
||||
plt.title('H2 Dissociation Curve')
|
||||
plt.show()
|
||||
```
|
||||
|
||||
## Basis Sets and Mapping
|
||||
|
||||
### Basis Set Selection
|
||||
|
||||
```python
|
||||
# Minimal basis (fastest, least accurate)
|
||||
H_sto3g, n_qubits = qchem.molecular_hamiltonian(
|
||||
symbols, coords, basis='sto-3g'
|
||||
)
|
||||
|
||||
# Double-zeta basis
|
||||
H_631g, n_qubits = qchem.molecular_hamiltonian(
|
||||
symbols, coords, basis='6-31g'
|
||||
)
|
||||
|
||||
# Large basis (slower, more accurate)
|
||||
H_ccpvdz, n_qubits = qchem.molecular_hamiltonian(
|
||||
symbols, coords, basis='cc-pvdz'
|
||||
)
|
||||
```
|
||||
|
||||
### Active Space Selection
|
||||
|
||||
```python
|
||||
# Select active orbitals
|
||||
active_electrons = 2
|
||||
active_orbitals = 2
|
||||
|
||||
H_active, n_qubits = qchem.molecular_hamiltonian(
|
||||
symbols,
|
||||
coords,
|
||||
active_electrons=active_electrons,
|
||||
active_orbitals=active_orbitals
|
||||
)
|
||||
|
||||
print(f"Full system: {len(symbols)} electrons")
|
||||
print(f"Active space: {active_electrons} electrons in {active_orbitals} orbitals")
|
||||
print(f"Qubits needed: {n_qubits}")
|
||||
```
|
||||
|
||||
### Fermion-to-Qubit Mappings
|
||||
|
||||
```python
|
||||
# Jordan-Wigner (default)
|
||||
H_jw, n_q_jw = qchem.molecular_hamiltonian(
|
||||
symbols, coords, mapping='jordan_wigner'
|
||||
)
|
||||
|
||||
# Bravyi-Kitaev
|
||||
H_bk, n_q_bk = qchem.molecular_hamiltonian(
|
||||
symbols, coords, mapping='bravyi_kitaev'
|
||||
)
|
||||
|
||||
# Parity
|
||||
H_parity, n_q_parity = qchem.molecular_hamiltonian(
|
||||
symbols, coords, mapping='parity'
|
||||
)
|
||||
|
||||
print(f"Jordan-Wigner terms: {len(H_jw.ops)}")
|
||||
print(f"Bravyi-Kitaev terms: {len(H_bk.ops)}")
|
||||
```
|
||||
|
||||
## Excited States
|
||||
|
||||
### Quantum Subspace Expansion
|
||||
|
||||
```python
|
||||
def quantum_subspace_expansion(hamiltonian, ground_state_params, excitations):
|
||||
"""Calculate excited states via subspace expansion."""
|
||||
|
||||
@qml.qnode(dev)
|
||||
def ground_state():
|
||||
ansatz(ground_state_params, wires=range(n_qubits))
|
||||
return qml.state()
|
||||
|
||||
# Get ground state
|
||||
psi_0 = ground_state()
|
||||
|
||||
# Generate excited state basis
|
||||
basis = [psi_0]
|
||||
|
||||
for exc in excitations:
|
||||
@qml.qnode(dev)
|
||||
def excited_state():
|
||||
ansatz(ground_state_params, wires=range(n_qubits))
|
||||
# Apply excitation
|
||||
apply_excitation(exc)
|
||||
return qml.state()
|
||||
|
||||
psi_exc = excited_state()
|
||||
basis.append(psi_exc)
|
||||
|
||||
# Build Hamiltonian matrix in subspace
|
||||
n_basis = len(basis)
|
||||
H_matrix = np.zeros((n_basis, n_basis))
|
||||
|
||||
for i in range(n_basis):
|
||||
for j in range(n_basis):
|
||||
H_matrix[i, j] = np.vdot(basis[i], hamiltonian @ basis[j])
|
||||
|
||||
# Diagonalize
|
||||
eigenvalues, eigenvectors = np.linalg.eigh(H_matrix)
|
||||
|
||||
return eigenvalues, eigenvectors
|
||||
```
|
||||
|
||||
### SSVQE (Subspace-Search VQE)
|
||||
|
||||
```python
|
||||
def ssvqe(hamiltonian, n_states=3):
|
||||
"""Calculate multiple states simultaneously."""
|
||||
|
||||
def cost_function(params):
|
||||
states = []
|
||||
|
||||
for i in range(n_states):
|
||||
@qml.qnode(dev)
|
||||
def state_i():
|
||||
ansatz(params[i], wires=range(n_qubits))
|
||||
return qml.state()
|
||||
|
||||
states.append(state_i())
|
||||
|
||||
# Energy expectation
|
||||
energies = [np.vdot(s, hamiltonian @ s) for s in states]
|
||||
|
||||
# Orthogonality penalty
|
||||
penalty = 0
|
||||
for i in range(n_states):
|
||||
for j in range(i+1, n_states):
|
||||
overlap = np.abs(np.vdot(states[i], states[j]))
|
||||
penalty += overlap ** 2
|
||||
|
||||
return sum(energies) + 1000 * penalty
|
||||
|
||||
# Initialize parameters for all states
|
||||
params = [np.random.random(n_params) for _ in range(n_states)]
|
||||
|
||||
opt = qml.AdamOptimizer(stepsize=0.01)
|
||||
for _ in range(100):
|
||||
params = opt.step(cost_function, params)
|
||||
|
||||
return params
|
||||
```
|
||||
|
||||
## Quantum Chemistry Workflows
|
||||
|
||||
### Full VQE Workflow
|
||||
|
||||
```python
|
||||
def full_chemistry_workflow(symbols, coords, basis='sto-3g'):
|
||||
"""Complete quantum chemistry calculation."""
|
||||
|
||||
print("1. Building molecular Hamiltonian...")
|
||||
H, n_qubits = qchem.molecular_hamiltonian(
|
||||
symbols, coords, basis=basis
|
||||
)
|
||||
|
||||
print(f" Molecule: {' '.join(symbols)}")
|
||||
print(f" Qubits: {n_qubits}")
|
||||
print(f" Hamiltonian terms: {len(H.ops)}")
|
||||
|
||||
print("\n2. Preparing Hartree-Fock state...")
|
||||
n_electrons = sum(qchem.atomic_numbers[s] for s in symbols)
|
||||
hf_state = qchem.hf_state(n_electrons, n_qubits)
|
||||
|
||||
print("\n3. Running VQE...")
|
||||
energy, params = run_vqe(H, n_qubits, hf_state)
|
||||
|
||||
print(f"\n4. Results:")
|
||||
print(f" Ground state energy: {energy:.8f} Ha")
|
||||
|
||||
print("\n5. Computing properties...")
|
||||
dipole = compute_dipole_moment(symbols, coords, params)
|
||||
print(f" Dipole moment: {dipole:.4f} D")
|
||||
|
||||
return {
|
||||
'energy': energy,
|
||||
'params': params,
|
||||
'dipole': dipole
|
||||
}
|
||||
|
||||
results = full_chemistry_workflow(['H', 'H'], h2_coords)
|
||||
```
|
||||
|
||||
### Molecular Property Calculation
|
||||
|
||||
```python
|
||||
def compute_molecular_properties(symbols, coords, vqe_params):
|
||||
"""Calculate molecular properties from VQE solution."""
|
||||
|
||||
# Energy
|
||||
H, n_qubits = qchem.molecular_hamiltonian(symbols, coords)
|
||||
energy = vqe_circuit(vqe_params)
|
||||
|
||||
# Dipole moment
|
||||
dipole_obs = qchem.dipole_moment(symbols, coords)
|
||||
|
||||
@qml.qnode(dev)
|
||||
def dipole_circuit(axis):
|
||||
ansatz(vqe_params, wires=range(n_qubits))
|
||||
return qml.expval(dipole_obs[axis])
|
||||
|
||||
dipole = [dipole_circuit(i) for i in range(3)]
|
||||
dipole_magnitude = np.linalg.norm(dipole)
|
||||
|
||||
# Particle number (sanity check)
|
||||
@qml.qnode(dev)
|
||||
def particle_number():
|
||||
ansatz(vqe_params, wires=range(n_qubits))
|
||||
N_op = qchem.particle_number(n_qubits)
|
||||
return qml.expval(N_op)
|
||||
|
||||
n_particles = particle_number()
|
||||
|
||||
return {
|
||||
'energy': energy,
|
||||
'dipole_moment': dipole_magnitude,
|
||||
'dipole_vector': dipole,
|
||||
'particle_number': n_particles
|
||||
}
|
||||
```
|
||||
|
||||
### Reaction Energy Calculation
|
||||
|
||||
```python
|
||||
def reaction_energy(reactants, products):
|
||||
"""Calculate energy of chemical reaction."""
|
||||
|
||||
# Calculate energies of reactants
|
||||
E_reactants = 0
|
||||
for molecule in reactants:
|
||||
symbols, coords = molecule
|
||||
H, n_qubits = qchem.molecular_hamiltonian(symbols, coords)
|
||||
E_reactants += run_vqe(H, n_qubits)
|
||||
|
||||
# Calculate energies of products
|
||||
E_products = 0
|
||||
for molecule in products:
|
||||
symbols, coords = molecule
|
||||
H, n_qubits = qchem.molecular_hamiltonian(symbols, coords)
|
||||
E_products += run_vqe(H, n_qubits)
|
||||
|
||||
# Reaction energy
|
||||
delta_E = E_products - E_reactants
|
||||
|
||||
print(f"Reactant energy: {E_reactants:.6f} Ha")
|
||||
print(f"Product energy: {E_products:.6f} Ha")
|
||||
print(f"Reaction energy: {delta_E:.6f} Ha ({delta_E * 627.5:.2f} kcal/mol)")
|
||||
|
||||
return delta_E
|
||||
|
||||
# Example: H2 dissociation
|
||||
reactants = [((['H', 'H'], h2_coords_bonded))]
|
||||
products = [((['H'], [0, 0, 0]), (['H'], [10, 0, 0]))] # Separated atoms
|
||||
|
||||
delta_E = reaction_energy(reactants, products)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Start with small basis sets** - Use STO-3G for testing, upgrade for production
|
||||
2. **Use active space** - Reduce qubits by selecting relevant orbitals
|
||||
3. **Choose appropriate mapping** - Bravyi-Kitaev often reduces circuit depth
|
||||
4. **Initialize with HF** - Start VQE from Hartree-Fock state
|
||||
5. **Validate results** - Compare with classical methods (FCI, CCSD)
|
||||
6. **Consider symmetries** - Exploit molecular symmetries to reduce complexity
|
||||
7. **Use UCCSD for accuracy** - UCCSD ansatz is chemically motivated
|
||||
8. **Monitor convergence** - Check gradient norms and energy variance
|
||||
9. **Account for correlation** - Ensure ansatz captures electron correlation
|
||||
10. **Benchmark thoroughly** - Test on known systems before novel molecules
|
||||
Reference in New Issue
Block a user