9  Ü2 · Hartree-Fock

SCF-Konvergenz, Roothaan-Gleichungen und Elektronenkorrelation

Autor:in

ZHAW Institut für Data Science

9.1 Lernziele

Nach dieser Übung kannst du:

  • SCF-Konvergenzverhalten analysieren und visualisieren
  • Überlappmatrix \(S\) und Fock-Matrix \(F\) direkt inspizieren
  • Den Einfluss des Basissatzes auf die HF-Energie systematisch untersuchen
  • Elektronenkorrelation über den Vergleich HF vs. MP2 quantifizieren
  • Das Koopmans-Theorem zur Vorhersage von Ionisierungspotentialen anwenden
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

plt.rcParams.update({'figure.dpi': 120, 'font.size': 11})

try:
    from pyscf import gto, scf, mp, ao2mo
    PYSCF_AVAILABLE = True
except ImportError:
    PYSCF_AVAILABLE = False
    print("PySCF nicht gefunden. Installiere mit: uv add pyscf")

9.2 Aufgabe 1 · SCF-Konvergenzverlauf

Das SCF-Verfahren iteriert bis zur Selbstkonsistenz. Beobachte den Verlauf der Gesamtenergie über die SCF-Iterationen für Wasser mit verschiedenen Basissätzen.

if not PYSCF_AVAILABLE:
    raise SystemExit("PySCF benötigt")

def h2o_mol(basis='sto-3g'):
    """Erzeugt ein H2O-Molekülobjekt mit der gewünschten Basis."""
    mol = gto.Mole()
    mol.atom = """
        O   0.000  0.000  0.000
        H   0.757  0.586  0.000
        H  -0.757  0.586  0.000
    """
    mol.basis = basis
    mol.verbose = 0
    mol.build()
    return mol


def run_scf_tracked(mol, max_cycle=50):
    """RHF mit Energieprotokoll pro Iteration."""
    mf = scf.RHF(mol)
    mf.max_cycle = max_cycle
    mf.conv_tol  = 1e-10
    
    energies = []
    
    def callback(envs):
        energies.append(envs['e_tot'])
    
    mf.callback = callback
    mf.kernel()
    return mf, np.array(energies)


# Vergleich verschiedener Basissätze
bases = ['sto-3g', '6-31g', '6-31g*', 'cc-pvdz']
results = {}

for basis in bases:
    mol = h2o_mol(basis)
    mf, energies = run_scf_tracked(mol)
    results[basis] = {'mf': mf, 'energies': energies, 'E': mf.e_tot}
    print(f"{basis:12s}: E = {mf.e_tot:.6f} Hartree  ({len(energies)} Iterationen)")
sto-3g      : E = -74.962947 Hartree  (7 Iterationen)
6-31g       : E = -75.983993 Hartree  (9 Iterationen)
6-31g*      : E = -76.009128 Hartree  (8 Iterationen)
cc-pvdz     : E = -76.026794 Hartree  (9 Iterationen)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

for basis, res in results.items():
    E_arr = res['energies']
    ax1.plot(range(1, len(E_arr)+1), E_arr, marker='o', ms=4, label=basis)
    
    # Konvergenz: Betrag der Energie-Änderung
    if len(E_arr) > 1:
        delta = np.abs(np.diff(E_arr))
        ax2.semilogy(range(1, len(delta)+1), delta, marker='s', ms=4, label=basis)

ax1.set_xlabel('SCF-Iteration')
ax1.set_ylabel('Gesamtenergie / Hartree')
ax1.set_title('SCF-Energieverlauf')
ax1.legend()

ax2.set_xlabel('SCF-Iteration')
ax2.set_ylabel('|ΔE| / Hartree')
ax2.set_title('Konvergenz (log-Skala)')
ax2.axhline(1e-10, ls='--', color='gray', label='Konvergenzkriterium')
ax2.legend(fontsize=9)

plt.suptitle('H₂O RHF – SCF-Konvergenz')
plt.tight_layout()
plt.show()


9.3 Aufgabe 2 · Überlappmatrix und Fock-Matrix

Inspiziere die Matrizen des SCF-Verfahrens direkt.

mol_sto3g = h2o_mol('sto-3g')
mf_sto3g, _ = run_scf_tracked(mol_sto3g)

# Matrizen aus dem konvergierten SCF
S = mol_sto3g.intor('int1e_ovlp')    # Überlappmatrix
F = mf_sto3g.get_fock()              # Fock-Matrix (konvergiert)
P = mf_sto3g.make_rdm1()             # Dichtematrix

ao_labels = [f"{ao[1]} {ao[2]}{ao[3]}" for ao in mol_sto3g.ao_labels(fmt=None)]

fig, axes = plt.subplots(1, 3, figsize=(14, 4))

for ax, mat, title in zip(axes, [S, F, P], ['Überlappmatrix S', 'Fock-Matrix F', 'Dichtematrix P']):
    im = ax.imshow(mat, cmap='RdBu_r', aspect='auto')
    plt.colorbar(im, ax=ax, shrink=0.8)
    ax.set_title(title)
    ax.set_xticks(range(len(ao_labels)))
    ax.set_yticks(range(len(ao_labels)))
    ax.set_xticklabels(ao_labels, rotation=45, ha='right', fontsize=8)
    ax.set_yticklabels(ao_labels, fontsize=8)

plt.suptitle('H₂O/STO-3G – Matrizen des SCF-Verfahrens')
plt.tight_layout()
plt.show()

print("\nBasisorbitale (AO-Labels):")
for i, l in enumerate(ao_labels):
    print(f"  {i}: {l}")


Basisorbitale (AO-Labels):
  0: O 1s
  1: O 2s
  2: O 2px
  3: O 2py
  4: O 2pz
  5: H 1s
  6: H 1s

9.4 Aufgabe 3 · Basissatz-Konvergenzstudie

Untersuche systematisch, wie die HF-Energie mit der Basissatzgrösse konvergiert.

basis_series = [
    ('STO-3G',    'sto-3g',    7),
    ('3-21G',     '3-21g',     13),
    ('6-31G',     '6-31g',     13),
    ('6-31G*',    '6-31g*',    19),
    ('6-31G**',   '6-31g**',   23),
    ('cc-pVDZ',   'cc-pvdz',   24),
    ('cc-pVTZ',   'cc-pvtz',   58),
]

# HF-Limit für H2O (Referenz aus Literatur): ~-76.0675 Hartree
E_hf_limit = -76.0675

data = []
for name, basis, n_ao in basis_series:
    mol = h2o_mol(basis)
    mf = scf.RHF(mol)
    mf.verbose = 0
    E = mf.kernel()
    data.append({'Basissatz': name, 'N_AO': n_ao, 'E_HF [Hartree]': E,
                 'ΔE_HF [mHartree]': (E - E_hf_limit)*1000})
    print(f"{name:12s}: {E:.6f} Hartree  (N_AO={n_ao})")

df_basis = pd.DataFrame(data)
print("\n", df_basis.to_string(index=False))
STO-3G      : -74.962947 Hartree  (N_AO=7)
3-21G       : -75.585395 Hartree  (N_AO=13)
6-31G       : -75.983993 Hartree  (N_AO=13)
6-31G*      : -76.009128 Hartree  (N_AO=19)
6-31G**     : -76.022641 Hartree  (N_AO=23)
cc-pVDZ     : -76.026794 Hartree  (N_AO=24)
cc-pVTZ     : -76.057161 Hartree  (N_AO=58)

 Basissatz  N_AO  E_HF [Hartree]  ΔE_HF [mHartree]
   STO-3G     7      -74.962947       1104.553343
    3-21G    13      -75.585395        482.104758
    6-31G    13      -75.983993         83.506772
   6-31G*    19      -76.009128         58.372212
  6-31G**    23      -76.022641         44.859063
  cc-pVDZ    24      -76.026794         40.706355
  cc-pVTZ    58      -76.057161         10.339319
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

ax1.plot(df_basis['N_AO'], df_basis['E_HF [Hartree]'], 'o-', color='steelblue', ms=7)
ax1.axhline(E_hf_limit, ls='--', color='red', label=f'HF-Limit ≈ {E_hf_limit:.4f} Eh')
for _, row in df_basis.iterrows():
    ax1.annotate(row['Basissatz'], (row['N_AO'], row['E_HF [Hartree]']),
                 textcoords='offset points', xytext=(5, 3), fontsize=8)
ax1.set_xlabel('Anzahl Basisorbitale')
ax1.set_ylabel('E_HF / Hartree')
ax1.set_title('HF-Energie vs. Basissatzgrösse')
ax1.legend()

ax2.semilogy(df_basis['N_AO'], np.abs(df_basis['ΔE_HF [mHartree]']), 'o-', color='darkorange', ms=7)
ax2.set_xlabel('Anzahl Basisorbitale')
ax2.set_ylabel('|E - E_HF_limit| / mHartree')
ax2.set_title('Abweichung vom HF-Limit (log)')

plt.suptitle('H₂O – Basissatz-Konvergenzstudie')
plt.tight_layout()
plt.show()


9.5 Aufgabe 4 · Koopmans-Theorem und Ionisierungspotentiale

Das Koopmans-Theorem besagt: Das negative der Orbitalenergie des HOMO entspricht dem (ersten) Ionisierungspotential: \[\text{IP} \approx -\varepsilon_{\text{HOMO}}\]

Vergleiche die Koopmans-Vorhersage mit dem Ergebnis aus einer direkten \(\Delta\)SCF-Rechnung.

molecules = {
    'H₂O': """O 0 0 0; H 0.757 0.586 0; H -0.757 0.586 0""",
    'NH₃': """N 0 0 0.116; H 0 0.939 -0.269; H 0.813 -0.469 -0.269; H -0.813 -0.469 -0.269""",
    'CH₄': """C 0 0 0; H 0.629 0.629 0.629; H -0.629 -0.629 0.629; H -0.629 0.629 -0.629; H 0.629 -0.629 -0.629""",
}

# Experimentelle erste Ionisierungspotentiale in eV (NIST)
IP_exp = {'H₂O': 12.62, 'NH₃': 10.82, 'CH₄': 12.61}

results_ip = []
for name, atom_str in molecules.items():
    # Neutrales Molekül
    mol = gto.Mole(atom=atom_str, basis='6-31g*', verbose=0)
    mol.build()
    mf = scf.RHF(mol)
    E_neutral = mf.kernel()
    
    # Koopmans-IP: -ε_HOMO
    homo_idx = int(mol.nelectron // 2) - 1
    IP_koopmans = -mf.mo_energy[homo_idx] * 27.211  # Hartree → eV
    
    # ΔSCF: Kation berechnen
    mol_cat = gto.Mole(atom=atom_str, basis='6-31g*', charge=1, spin=1, verbose=0)
    mol_cat.build()
    mf_cat = scf.UHF(mol_cat)
    E_cation = mf_cat.kernel()
    
    IP_delta = (E_cation - E_neutral) * 27.211  # Hartree → eV
    
    results_ip.append({
        'Molekül': name,
        'IP Koopmans [eV]': IP_koopmans,
        'IP ΔSCF [eV]': IP_delta,
        'IP exp. [eV]': IP_exp[name],
        'Fehler Koopmans [eV]': IP_koopmans - IP_exp[name],
        'Fehler ΔSCF [eV]': IP_delta - IP_exp[name],
    })
    print(f"{name}: Koopmans={IP_koopmans:.2f} eV | ΔSCF={IP_delta:.2f} eV | exp={IP_exp[name]:.2f} eV")

df_ip = pd.DataFrame(results_ip)
print()
print(df_ip.to_string(index=False))
H₂O: Koopmans=13.54 eV | ΔSCF=10.85 eV | exp=12.62 eV
NH₃: Koopmans=11.47 eV | ΔSCF=9.29 eV | exp=10.82 eV
CH₄: Koopmans=14.82 eV | ΔSCF=13.38 eV | exp=12.61 eV

Molekül  IP Koopmans [eV]  IP ΔSCF [eV]  IP exp. [eV]  Fehler Koopmans [eV]  Fehler ΔSCF [eV]
    H₂O         13.539739     10.848314         12.62              0.919739         -1.771686
    NH₃         11.473014      9.293784         10.82              0.653014         -1.526216
    CH₄         14.823016     13.377483         12.61              2.213016          0.767483

9.6 Aufgabe 5 · Elektronenkorrelation: HF vs. MP2

Berechne die Korrelationsenergie \(E_\text{corr} = E_\text{MP2} - E_\text{HF}\) für eine Reihe kleiner Moleküle.

Die MP2-Methode (Møller-Plesset 2. Ordnung) ist die einfachste post-HF-Methode und erfasst dynamische Elektronenkorrelation.

from pyscf import mp

molecules_corr = {
    'H₂':  "H 0 0 0; H 0 0 0.741",
    'H₂O': "O 0 0 0; H 0.757 0.586 0; H -0.757 0.586 0",
    'NH₃': "N 0 0 0.116; H 0 0.939 -0.269; H 0.813 -0.469 -0.269; H -0.813 -0.469 -0.269",
    'HF':  "F 0 0 0; H 0 0 0.917",
}

corr_data = []
for name, atom_str in molecules_corr.items():
    mol = gto.Mole(atom=atom_str, basis='cc-pvdz', verbose=0)
    mol.build()
    
    # HF
    mf = scf.RHF(mol)
    E_hf = mf.kernel()
    
    # MP2
    mp2 = mp.MP2(mf)
    E_corr, _ = mp2.kernel()
    E_mp2 = E_hf + E_corr
    
    corr_data.append({
        'Molekül': name,
        'E_HF [Eh]':        E_hf,
        'E_MP2 [Eh]':       E_mp2,
        'E_corr [mEh]':     E_corr * 1000,
        'E_corr [kJ/mol]':  E_corr * 2625.5,
    })
    print(f"{name:6s}: E_HF={E_hf:.4f}, E_corr={E_corr*1000:.1f} mEh ({E_corr*2625.5:.1f} kJ/mol)")

df_corr = pd.DataFrame(corr_data)

# Visualisierung
fig, ax = plt.subplots(figsize=(8, 5))
x = np.arange(len(df_corr))
ax.bar(x, np.abs(df_corr['E_corr [kJ/mol]']), color='steelblue', alpha=0.8)
ax.set_xticks(x)
ax.set_xticklabels(df_corr['Molekül'], fontsize=12)
ax.set_ylabel('|E_corr| / kJ mol⁻¹')
ax.set_title('Korrelationsenergie (MP2/cc-pVDZ)')
for i, v in enumerate(np.abs(df_corr['E_corr [kJ/mol]'])):
    ax.text(i, v + 5, f'{v:.0f}', ha='center', fontsize=10)
plt.tight_layout()
plt.show()
H₂    : E_HF=-1.1287, E_corr=-26.4 mEh (-69.3 kJ/mol)
H₂O   : E_HF=-76.0268, E_corr=-204.0 mEh (-535.5 kJ/mol)
NH₃   : E_HF=-56.1956, E_corr=-189.1 mEh (-496.6 kJ/mol)
HF    : E_HF=-100.0194, E_corr=-203.8 mEh (-535.0 kJ/mol)

9.6.1 Reflexionsfragen

  1. Welches Molekül zeigt die grösste absolute Korrelationsenergie? Warum?
  2. Warum ist die Korrelationsenergie immer negativ?
  3. Das Koopmans-Theorem überschätzt typischerweise das IP. Warum systematisch?
  4. Welche Näherung wird im HF-Verfahren durch die Slater-Determinante gemacht, und was folgt daraus für die Elektronenkorrelation?

9.7 Zusammenfassung

In dieser Übung hast du:

  • SCF-Konvergenzkurven analysiert und den Einfluss des Basissatzes quantifiziert
  • Überlapp-, Fock- und Dichtematrix direkt inspiziert
  • Ionisierungspotentiale via Koopmans-Theorem und ΔSCF berechnet
  • Elektronenkorrelationsenergien auf MP2-Niveau berechnet und verglichen