11  Ü4 · Molekulare Eigenschaften

Dipolmoment, Geometrieoptimierung und Schwingungsfrequenzen

Autor:in

ZHAW Institut für Data Science

11.1 Lernziele

Nach dieser Übung kannst du:

  • Elektrische Eigenschaften (Dipolmoment, Polarisierbarkeit) berechnen und interpretieren
  • Geometrieoptimierungen durchführen und konvergierte Strukturen analysieren
  • Potentialenergiekurven (PES) für einfache Systeme berechnen
  • Mulliken- und Löwdin-Ladungen vergleichen
  • Schwingungsfrequenzen aus numerischen Hessian-Matrizen bestimmen
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

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

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

11.2 Aufgabe 1 · Dipolmomente

Das Dipolmoment \(\boldsymbol{\mu}\) ist eine der wichtigsten molekularen Eigenschaften: \[\mu_\alpha = -\sum_i \langle\psi_i|r_\alpha|\psi_i\rangle + \sum_A Z_A R_{A\alpha}\]

Berechne Dipolmomente für eine Reihe polarer und unpolarer Moleküle.

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

# 1 Debye = 0.393430 a.u.
AU_TO_DEBYE = 2.5418

molecules = {
    'H₂O':  ('O 0 0 0; H 0.757 0.586 0; H -0.757 0.586 0', 0, 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', 0, 0),
    'HF':   ('F 0 0 0; H 0 0 0.917', 0, 0),
    'HCl':  ('Cl 0 0 0; H 0 0 1.275', 0, 0),
    'CO':   ('C 0 0 0; O 0 0 1.128', 0, 0),
    'H₂':   ('H 0 0 -0.371; H 0 0 0.371', 0, 0),
    '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', 0, 0),
}

# Experimentelle Dipolmomente in Debye
dip_exp = {'H₂O': 1.85, 'NH₃': 1.47, 'HF': 1.83, 'HCl': 1.11, 'CO': 0.11, 'H₂': 0.00, 'CH₄': 0.00}

results_dip = []
for name, (atom_str, charge, spin) in molecules.items():
    mol = gto.Mole(atom=atom_str, basis='6-31g*', charge=charge, spin=spin, verbose=0)
    mol.build()
    
    mf = dft.RKS(mol, xc='b3lyp')
    mf.verbose = 0
    mf.kernel()
    
    dip = mf.dip_moment(verbose=0)  # in Debye
    dip_norm = np.linalg.norm(dip)
    
    results_dip.append({
        'Molekül': name,
        '|μ| calc [D]': dip_norm,
        '|μ| exp [D]': dip_exp[name],
        'Fehler [D]': dip_norm - dip_exp[name],
    })
    print(f"{name:6s}: μ = {dip_norm:.3f} D (exp: {dip_exp[name]:.2f} D)")

df_dip = pd.DataFrame(results_dip)
print()
print(df_dip.to_string(index=False))
H₂O   : μ = 2.073 D (exp: 1.85 D)
NH₃   : μ = 1.866 D (exp: 1.47 D)
HF    : μ = 1.842 D (exp: 1.83 D)
HCl   : μ = 1.461 D (exp: 1.11 D)
CO    : μ = 0.097 D (exp: 0.11 D)
H₂    : μ = 0.000 D (exp: 0.00 D)
CH₄   : μ = 0.000 D (exp: 0.00 D)

Molekül  |μ| calc [D]  |μ| exp [D]    Fehler [D]
    H₂O  2.073118e+00         1.85  2.231180e-01
    NH₃  1.865791e+00         1.47  3.957909e-01
     HF  1.842358e+00         1.83  1.235794e-02
    HCl  1.460798e+00         1.11  3.507978e-01
     CO  9.657370e-02         0.11 -1.342630e-02
     H₂  2.072775e-15         0.00  2.072775e-15
    CH₄  3.248640e-14         0.00  3.248640e-14
fig, ax = plt.subplots(figsize=(9, 5))

x = np.arange(len(df_dip))
ax.bar(x - 0.2, df_dip['|μ| calc [D]'], 0.35, label='B3LYP/6-31G*', color='steelblue', alpha=0.85)
ax.bar(x + 0.2, df_dip['|μ| exp [D]'],  0.35, label='Experiment',   color='darkorange', alpha=0.85)
ax.set_xticks(x)
ax.set_xticklabels(df_dip['Molekül'], fontsize=12)
ax.set_ylabel('Dipolmoment |μ| / Debye')
ax.set_title('Dipolmomente: B3LYP/6-31G* vs. Experiment')
ax.legend()
plt.tight_layout()
plt.show()

11.3 Aufgabe 2 · Mulliken- und Löwdin-Ladungen

Populationsanalysen ordnen Elektronen formal den Atomen zu. Vergleiche Mulliken- und Löwdin-Ladungen.

from pyscf.lo import orth

def lowdin_charges(mol, dm):
    """Löwdin-Populationsanalyse: orthogonalisierte Basis via S^{-1/2}."""
    S = mol.intor('int1e_ovlp')
    # S^{1/2}
    eigval, eigvec = np.linalg.eigh(S)
    S_half = eigvec @ np.diag(np.sqrt(eigval)) @ eigvec.T
    # Dichtematrix in orthogonaler Basis
    dm_orth = S_half @ dm @ S_half
    # Populationen pro AO
    ao_pop = np.diag(dm_orth)
    # Summierung über Schalen pro Atom
    atom_charges = np.array([mol.atom_charge(i) for i in range(mol.natm)], dtype=float)
    for i, label in enumerate(mol.ao_labels(fmt=None)):
        atom_idx = label[0]
        atom_charges[atom_idx] -= ao_pop[i]
    return atom_charges


test_molecules = {
    'H₂O':  "O 0 0 0; H 0.757 0.586 0; H -0.757 0.586 0",
    'HF':   "F 0 0 0; H 0 0 0.917",
    '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",
}

for mol_name, atom_str in test_molecules.items():
    mol = gto.Mole(atom=atom_str, basis='6-31g*', verbose=0)
    mol.build()
    mf = dft.RKS(mol, xc='b3lyp')
    mf.verbose = 0
    mf.kernel()
    dm = mf.make_rdm1()
    
    # Mulliken
    mull_pop, mull_charges = mf.mulliken_pop(verbose=0)
    
    # Löwdin
    low_charges = lowdin_charges(mol, dm)
    
    print(f"\n{mol_name} (B3LYP/6-31G*):")
    print(f"  {'Atom':>6} | {'Mulliken':>10} | {'Löwdin':>10}")
    print("  " + "-" * 32)
    for i in range(mol.natm):
        print(f"  {mol.atom_symbol(i):>6} | {mull_charges[i]:>10.4f} | {low_charges[i]:>10.4f}")

H₂O (B3LYP/6-31G*):
    Atom |   Mulliken |     Löwdin
  --------------------------------
       O |    -0.7978 |    -0.6158
       H |     0.3989 |     0.3079
       H |     0.3989 |     0.3079

HF (B3LYP/6-31G*):
    Atom |   Mulliken |     Löwdin
  --------------------------------
       F |    -0.4878 |    -0.3800
       H |     0.4878 |     0.3800

NH₃ (B3LYP/6-31G*):
    Atom |   Mulliken |     Löwdin
  --------------------------------
       N |    -0.8983 |    -0.6704
       H |     0.2993 |     0.2235
       H |     0.2995 |     0.2235
       H |     0.2995 |     0.2235

11.4 Aufgabe 3 · Potentialenergiekurve: H₂-Bindungsdissoziation

Berechne die Potentialenergiekurve (PES) für die H-H-Streckbewegung und vergleiche HF mit DFT.

r_vals = np.linspace(0.5, 5.0, 30)  # H-H-Abstand in Angström

E_hf   = np.zeros(len(r_vals))
E_b3lyp = np.zeros(len(r_vals))

for i, r in enumerate(r_vals):
    mol = gto.Mole(
        atom=f"H 0 0 0; H 0 0 {r}",
        basis='cc-pvdz', verbose=0, spin=0
    )
    mol.build()
    
    # RHF
    mf = scf.RHF(mol)
    mf.verbose = 0
    E_hf[i] = mf.kernel()
    
    # B3LYP
    mf_dft = dft.RKS(mol, xc='b3lyp')
    mf_dft.verbose = 0
    E_b3lyp[i] = mf_dft.kernel()

# Dissoziation: Energie relativ zum Gleichgewichtsabstand
E_hf_rel    = (E_hf    - E_hf.min())    * 2625.5  # kJ/mol
E_b3lyp_rel = (E_b3lyp - E_b3lyp.min()) * 2625.5  # kJ/mol

# Gleichgewichtsabstand
r_eq_hf    = r_vals[E_hf.argmin()]
r_eq_b3lyp = r_vals[E_b3lyp.argmin()]

print(f"Gleichgewichtsabstand RHF:    {r_eq_hf:.3f} Å")
print(f"Gleichgewichtsabstand B3LYP:  {r_eq_b3lyp:.3f} Å")
print(f"Experiment:                   0.741 Å")

fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(r_vals, E_hf_rel,    'o-', color='steelblue',  label='RHF/cc-pVDZ', lw=2)
ax.plot(r_vals, E_b3lyp_rel, 's-', color='darkorange', label='B3LYP/cc-pVDZ', lw=2)
ax.axvline(0.741, ls='--', color='gray', alpha=0.7, label='r_exp = 0.741 Å')
ax.set_xlabel('H-H-Abstand / Å')
ax.set_ylabel('Relative Energie / kJ mol⁻¹')
ax.set_title('H₂-Potentialenergiekurve: RHF vs. B3LYP')
ax.set_ylim(-20, 500)
ax.legend()
plt.tight_layout()
plt.show()

print("\nNote: RHF bricht die Dissoziation nicht korrekt ab (ionische Grenzstruktur dominiert)")
print("DFT beschreibt die Dissoziation ebenfalls nicht exakt korrekt (statische Korrelation)")
Gleichgewichtsabstand RHF:    0.810 Å
Gleichgewichtsabstand B3LYP:  0.810 Å
Experiment:                   0.741 Å

Note: RHF bricht die Dissoziation nicht korrekt ab (ionische Grenzstruktur dominiert)
DFT beschreibt die Dissoziation ebenfalls nicht exakt korrekt (statische Korrelation)

11.5 Aufgabe 4 · Geometrieoptimierung

Optimiere die Geometrie von H₂O mit B3LYP/6-31G* und vergleiche die Ergebnisse mit dem Experiment.

try:
    from pyscf.geomopt.berny_solver import optimize
    GEOMOPT = True
except ImportError:
    try:
        from pyscf.geomopt.geometric_solver import optimize
        GEOMOPT = True
    except ImportError:
        GEOMOPT = False
        print("Geometrieoptimierung benötigt: uv add geometric")

if GEOMOPT:
    # Startgeometrie (leicht gestört vom Gleichgewicht)
    mol = gto.Mole(
        atom="O 0 0 0; H 0.800 0.600 0; H -0.800 0.600 0",
        basis='6-31g*', verbose=0
    )
    mol.build()
    
    mf = dft.RKS(mol, xc='b3lyp')
    mf.verbose = 0
    
    mol_opt = optimize(mf)
    
    # Optimierte Koordinaten (in Angström)
    coords_opt = mol_opt.atom_coords() * 0.529177  # Bohr → Angström
    
    # O-H-Abstand und H-O-H-Winkel berechnen
    O = coords_opt[0]
    H1 = coords_opt[1]
    H2 = coords_opt[2]
    
    r_OH1 = np.linalg.norm(H1 - O)
    r_OH2 = np.linalg.norm(H2 - O)
    
    v1 = (H1 - O) / r_OH1
    v2 = (H2 - O) / r_OH2
    angle = np.degrees(np.arccos(np.clip(np.dot(v1, v2), -1, 1)))
    
    print("Optimierte Geometrie (B3LYP/6-31G*):")
    print(f"  r(O-H):      {r_OH1:.4f} Å   (exp: 0.9572 Å)")
    print(f"  ∠(H-O-H):   {angle:.2f}°   (exp: 104.52°)")
else:
    print("geometric nicht installiert – Geometrieoptimierung übersprungen.")
    print("Installiere mit: uv add geometric")
Geometrieoptimierung benötigt: uv add geometric
geometric nicht installiert – Geometrieoptimierung übersprungen.
Installiere mit: uv add geometric

11.6 Aufgabe 5 · Schwingungsfrequenz: H₂ via numerischem Hessian

Berechne die Streckschwingungsfrequenz von H₂ via numerischer zweiter Ableitung der Energie.

# Numerischer Hessian für H₂ (nur eine Koordinate: H-H-Abstand)
# E(r) ≈ E(r0) + 1/2 * k * (r - r0)^2  →  k = d²E/dr²

def E_h2(r_angstrom, xc='b3lyp', basis='cc-pvdz'):
    mol = gto.Mole(
        atom=f"H 0 0 0; H 0 0 {r_angstrom}",
        basis=basis, verbose=0
    )
    mol.build()
    mf = dft.RKS(mol, xc=xc)
    mf.verbose = 0
    return mf.kernel()

# Gleichgewichtsabstand (optimiert oder experimentell)
r0 = 0.742   # Angström
dr = 0.005   # Schrittweite

E_plus  = E_h2(r0 + dr)
E_0     = E_h2(r0)
E_minus = E_h2(r0 - dr)

# Numerische zweite Ableitung (Hartree / Ų)
k_hartree_ang = (E_plus - 2*E_0 + E_minus) / dr**2

# Einheitenumrechnung → SI
# 1 Hartree/Bohr² = 1556 N/m
# 1 Bohr = 0.529177e-10 m
# 1 Hartree = 4.3597e-18 J

hartree_per_bohr2_to_Nm = 1556.0
ang_to_bohr = 1.0 / 0.529177
k_hartree_bohr2 = k_hartree_ang * ang_to_bohr**2
k_SI = k_hartree_bohr2 * hartree_per_bohr2_to_Nm  # N/m

# Reduzierte Masse von H₂ (1 amu)
mu_amu = 0.5 * 1.00794     # amu
mu_kg  = mu_amu * 1.66054e-27  # kg

# Kreisfrequenz → Frequenz in cm⁻¹
omega = np.sqrt(k_SI / mu_kg)          # rad/s
nu_hz = omega / (2 * np.pi)            # Hz
nu_cm = nu_hz / (2.998e10)             # cm⁻¹

print(f"Kraftkonstante k:        {k_SI:.1f} N/m")
print(f"Schwingungsfrequenz ν̃:  {nu_cm:.0f} cm⁻¹")
print(f"Experimentell (H₂):     4161 cm⁻¹")
Kraftkonstante k:        8164.8 N/m
Schwingungsfrequenz ν̃:  16582 cm⁻¹
Experimentell (H₂):     4161 cm⁻¹

11.6.1 Reflexionsfragen

  1. Warum ist die Mulliken-Ladung basissatzabhängig, die Löwdin-Ladung dagegen stabiler?
  2. Warum beschreibt RHF die H₂-Dissoziation falsch? Welche Methode wäre besser geeignet?
  3. Welche Einflüsse werden bei der harmonischen Näherung für die Schwingungsfrequenz vernachlässigt?
  4. Das berechnete Dipolmoment von CO ist kleiner als erwartet. Was zeigt das über die Genauigkeit einfacher Basissätze?

11.7 Zusammenfassung

In dieser Übung hast du:

  • Dipolmomente für polare und unpolare Moleküle berechnet und mit Experimenten verglichen
  • Mulliken- und Löwdin-Ladungen verglichen und Stärken/Schwächen diskutiert
  • Eine Potentialenergiekurve für H₂-Dissoziation mit RHF und B3LYP berechnet
  • Geometrieoptimierung und Schwingungsfrequenzen numerisch bestimmt