Skip to content

Commit 0d5eed0

Browse files
Alex Epsteinpre-commit-ci[bot]rkingsburyjanosh
authored
Quasi-RRHO Thermochemistry Analysis Module (#2028)
* Linear molecules + units Tried to use more built-in units. Added functionality for checking if linear and adjusting rotational entropy accordingly. Added testing for linear molecule * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * pylint debugging Tried to make avg_mom_inertia an internal function * Pylint fixing Moved get_avg_mom_inertia outside the class * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * black * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * black 22.1.0 * QuasiRRHO: edits * QuasiRRHO: update tests * pre-commit auto-fixes * Incorporating comments - Cleaned up constants to use scipy.const - Created general __init__ and added functions from_GuassianOutput and from_QCOutput - Updated to google style docstrings * pre-commit Running pre-commit * Typos and doi fixed typo in units added duecredit decorator * docs update 1M in docs * duecredit fixing bugs * pre-commit auto-fixes * Remove Concentration Correction Removed solvent concentration corrections from the code so the class only deals with QuasiRRHO corrections * pre-commit auto-fixes * Changes to testing * pre-commit auto-fixes * add type hints and snake_case QuasiRRHO method names * rename single-letter module-scoped vars * fix tests following method renaming * add test_extreme_temperature_and_pressure() and test_get_avg_mom_inertia() --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Ryan Kingsbury <[email protected]> Co-authored-by: Janosh Riebesell <[email protected]>
1 parent b662715 commit 0d5eed0

File tree

3 files changed

+341
-0
lines changed

3 files changed

+341
-0
lines changed

pymatgen/analysis/quasirrho.py

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
# Copyright (c) Pymatgen Development Team.
2+
# Distributed under the terms of the MIT License.
3+
4+
5+
"""
6+
A module to calculate free energies using the Quasi-Rigid Rotor Harmonic
7+
Oscillator approximation. Modified from a script by Steven Wheeler.
8+
See: Grimme, S. Chem. Eur. J. 2012, 18, 9955
9+
"""
10+
11+
from __future__ import annotations
12+
13+
__author__ = "Alex Epstein"
14+
__copyright__ = "Copyright 2020, The Materials Project"
15+
__version__ = "0.1"
16+
__maintainer__ = "Alex Epstein"
17+
__email__ = "[email protected]"
18+
__date__ = "August 1, 2023"
19+
__credits__ = "Ryan Kingsbury, Steven Wheeler, Trevor Seguin, Evan Spotte-Smith"
20+
21+
from math import isclose
22+
from typing import TYPE_CHECKING
23+
24+
import numpy as np
25+
import scipy.constants as const
26+
27+
from pymatgen.core.units import kb as kb_ev
28+
from pymatgen.util.due import Doi, due
29+
30+
if TYPE_CHECKING:
31+
from pymatgen.core import Molecule
32+
from pymatgen.io.gaussian import GaussianOutput
33+
from pymatgen.io.qchem.outputs import QCOutput
34+
35+
# Define useful constants
36+
kb = kb_ev * const.eV # Pymatgen kb [J/K]
37+
light_speed = const.speed_of_light * 100 # [cm/s]
38+
h_plank = const.h # Planck's constant [J.s]
39+
ideal_gas_const = const.R / const.calorie # Ideal gas constant [cal/mol/K]
40+
R_ha = const.R / const.value("Hartree energy") / const.Avogadro # Ideal gas
41+
42+
# Define useful conversion factors
43+
amu_to_kg = const.value("atomic mass unit-kilogram relationship") # AMU to kg
44+
# kcal2hartree = 0.0015936 # kcal/mol to hartree/mol
45+
kcal_to_hartree = 1000 * const.calorie / const.value("Hartree energy") / const.Avogadro
46+
47+
48+
def get_avg_mom_inertia(mol):
49+
"""
50+
Calculate the average moment of inertia of a molecule
51+
52+
Args:
53+
mol (Molecule): Pymatgen Molecule
54+
55+
Returns:
56+
int, list: average moment of inertia, eigenvalues of the inertia tensor
57+
"""
58+
centered_mol = mol.get_centered_molecule()
59+
inertia_tensor = np.zeros((3, 3))
60+
for site in centered_mol:
61+
c = site.coords
62+
wt = site.specie.atomic_mass
63+
for dim in range(3):
64+
inertia_tensor[dim, dim] += wt * (c[(dim + 1) % 3] ** 2 + c[(dim + 2) % 3] ** 2)
65+
for ii, jj in [(0, 1), (1, 2), (0, 2)]:
66+
inertia_tensor[ii, jj] += -wt * c[ii] * c[jj]
67+
inertia_tensor[jj, ii] += -wt * c[jj] * c[ii]
68+
69+
# amuangs^2 to kg m^2
70+
inertia_eigen_vals = np.linalg.eig(inertia_tensor)[0] * amu_to_kg * 1e-20
71+
72+
iav = np.average(inertia_eigen_vals)
73+
74+
return iav, inertia_eigen_vals
75+
76+
77+
class QuasiRRHO:
78+
"""
79+
Class to calculate thermochemistry using Grimme's Quasi-RRHO approximation.
80+
All outputs are in atomic units, e.g. energy outputs are in Hartrees.
81+
Citation: Grimme, S. Chemistry - A European Journal 18, 9955-9964 (2012).
82+
83+
Attributes:
84+
temp (float): Temperature [K]
85+
press (float): Pressure [Pa]
86+
v0 (float): Cutoff frequency for Quasi-RRHO method [1/cm]
87+
entropy_quasiRRHO (float): Quasi-RRHO entropy [Ha/K]
88+
entropy_ho (float): Total entropy calculated with a harmonic
89+
oscillator approximation for the vibrational entropy [Ha/K]
90+
h_corrected (float): Thermal correction to the enthalpy [Ha]
91+
free_energy_quasiRRHO (float): Quasi-RRHO free energy [Ha]
92+
free_energy_ho (float): Free energy calculated without the Quasi-RRHO
93+
method, i.e. with a harmonic oscillator approximation for the
94+
vibrational entropy [Ha]
95+
96+
"""
97+
98+
def __init__(
99+
self,
100+
mol: Molecule,
101+
frequencies: list[float],
102+
energy: float,
103+
mult: int,
104+
sigma_r: float = 1,
105+
temp: float = 298.15,
106+
press: float = 101_317,
107+
v0: float = 100,
108+
) -> None:
109+
"""
110+
Args:
111+
mol (Molecule): Pymatgen molecule
112+
frequencies (list): List of frequencies (float) [cm^-1]
113+
energy (float): Electronic energy [Ha]
114+
mult (int): Spin multiplicity
115+
sigma_r (int): Rotational symmetry number. Defaults to 1.
116+
temp (float): Temperature [K]. Defaults to 298.15.
117+
press (float): Pressure [Pa]. Defaults to 101_317.
118+
v0 (float): Cutoff frequency for Quasi-RRHO method [cm^-1]. Defaults to 100.
119+
"""
120+
# TO-DO: calculate sigma_r with PointGroupAnalyzer
121+
# and/or edit Gaussian and QChem io to parse for sigma_r
122+
123+
self.temp = temp
124+
self.press = press
125+
self.v0 = v0
126+
127+
self.entropy_quasiRRHO = None # Ha/K
128+
self.free_energy_quasiRRHO = None # Ha
129+
self.h_corrected = None # Ha
130+
131+
self.entropy_ho = None # Ha/K
132+
self.free_energy_ho = None # Ha
133+
134+
self._get_quasirrho_thermo(mol=mol, mult=mult, frequencies=frequencies, elec_energy=energy, sigma_r=sigma_r)
135+
136+
@classmethod
137+
def from_gaussian_output(cls, output: GaussianOutput, **kwargs) -> QuasiRRHO:
138+
"""
139+
140+
Args:
141+
output (GaussianOutput): Pymatgen GaussianOutput object
142+
143+
Returns:
144+
QuasiRRHO: QuasiRRHO class instantiated from a Gaussian Output
145+
"""
146+
mult = output.spin_multiplicity
147+
elec_e = output.final_energy
148+
mol = output.final_structure
149+
vib_freqs = [f["frequency"] for f in output.frequencies[-1]]
150+
return cls(mol=mol, frequencies=vib_freqs, energy=elec_e, mult=mult, **kwargs)
151+
152+
@classmethod
153+
def from_qc_output(cls, output: QCOutput, **kwargs) -> QuasiRRHO:
154+
"""
155+
156+
Args:
157+
output (QCOutput): Pymatgen QCOutput object
158+
159+
Returns:
160+
QuasiRRHO: QuasiRRHO class instantiated from a QChem Output
161+
162+
"""
163+
mult = output.data["multiplicity"]
164+
elec_e = output.data["SCF_energy_in_the_final_basis_set"]
165+
if output.data["optimization"]:
166+
mol = output.data["molecule_from_last_geometry"]
167+
else:
168+
mol = output.data["initial_molecule"]
169+
frequencies = output.data["frequencies"]
170+
171+
return cls(mol=mol, frequencies=frequencies, energy=elec_e, mult=mult, **kwargs)
172+
173+
@due.dcite(
174+
Doi("10.1002/chem.201200497"),
175+
description="Supramolecular Binding Thermodynamics by Dispersion-Corrected Density Functional Theory",
176+
)
177+
def _get_quasirrho_thermo(
178+
self, mol: Molecule, mult: int, sigma_r: int, frequencies: list[float], elec_energy: float
179+
) -> None:
180+
"""
181+
Calculate Quasi-RRHO thermochemistry
182+
183+
Args:
184+
mol (Molecule): Pymatgen molecule
185+
mult (int): Spin multiplicity
186+
sigma_r (int): Rotational symmetry number
187+
frequencies (list): List of frequencies [cm^-1]
188+
elec_energy (float): Electronic energy [Ha]
189+
"""
190+
# Calculate mass in kg
191+
mass: float = 0
192+
for site in mol.sites:
193+
mass += site.specie.atomic_mass
194+
mass *= amu_to_kg
195+
196+
# Calculate vibrational temperatures
197+
vib_temps = [freq * light_speed * h_plank / kb for freq in frequencies if freq > 0]
198+
199+
# Translational component of entropy and energy
200+
qt = (2 * np.pi * mass * kb * self.temp / (h_plank * h_plank)) ** (3 / 2) * kb * self.temp / self.press
201+
st = ideal_gas_const * (np.log(qt) + 5 / 2)
202+
et = 3 * ideal_gas_const * self.temp / 2
203+
204+
# Electronic component of Entropy
205+
se = ideal_gas_const * np.log(mult)
206+
207+
# Get properties related to rotational symmetry. Bav is average moment of inertia
208+
Bav, i_eigen = get_avg_mom_inertia(mol)
209+
210+
# Check if linear
211+
coords = mol.cart_coords
212+
v0 = coords[1] - coords[0]
213+
linear = True
214+
for coord in coords[1:]:
215+
theta = abs(np.dot(coord - coords[0], v0) / np.linalg.norm(coord - coords[0]) / np.linalg.norm(v0))
216+
if not isclose(theta, 1, abs_tol=1e-4):
217+
linear = False
218+
219+
# Rotational component of Entropy and Energy
220+
if linear:
221+
i = np.amax(i_eigen)
222+
qr = 8 * np.pi**2 * i * kb * self.temp / (sigma_r * (h_plank * h_plank))
223+
sr = ideal_gas_const * (np.log(qr) + 1)
224+
er = ideal_gas_const * self.temp
225+
else:
226+
rot_temps = [h_plank**2 / (np.pi**2 * kb * 8 * i) for i in i_eigen]
227+
qr = np.sqrt(np.pi) / sigma_r * self.temp ** (3 / 2) / np.sqrt(rot_temps[0] * rot_temps[1] * rot_temps[2])
228+
sr = ideal_gas_const * (np.log(qr) + 3 / 2)
229+
er = 3 * ideal_gas_const * self.temp / 2
230+
231+
# Vibrational component of Entropy and Energy
232+
ev = 0
233+
sv_quasiRRHO = 0
234+
sv = 0
235+
236+
for vt in vib_temps:
237+
ev += vt * (1 / 2 + 1 / (np.exp(vt / self.temp) - 1))
238+
sv_temp = vt / (self.temp * (np.exp(vt / self.temp) - 1)) - np.log(1 - np.exp(-vt / self.temp))
239+
sv += sv_temp
240+
241+
mu = h_plank / (8 * np.pi**2 * vt * light_speed)
242+
mu_prime = mu * Bav / (mu + Bav)
243+
s_rotor = 1 / 2 + np.log(np.sqrt(8 * np.pi**3 * mu_prime * kb * self.temp / h_plank**2))
244+
weight = 1 / (1 + (self.v0 / vt) ** 4)
245+
sv_quasiRRHO += weight * sv_temp + (1 - weight) * s_rotor
246+
247+
sv_quasiRRHO *= ideal_gas_const
248+
sv *= ideal_gas_const
249+
ev *= ideal_gas_const
250+
e_tot = (et + er + ev) * kcal_to_hartree / 1000
251+
self.h_corrected = e_tot + ideal_gas_const * self.temp * kcal_to_hartree / 1000
252+
self.entropy_ho = st + sr + sv + se
253+
self.free_energy_ho = elec_energy + self.h_corrected - (self.temp * self.entropy_ho * kcal_to_hartree / 1000)
254+
self.entropy_quasiRRHO = st + sr + sv_quasiRRHO + se
255+
self.free_energy_quasiRRHO = (
256+
elec_energy + self.h_corrected - (self.temp * self.entropy_quasiRRHO * kcal_to_hartree / 1000)
257+
)

tests/analysis/test_quasirrho.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
from __future__ import annotations
2+
3+
import unittest
4+
5+
import pytest
6+
7+
from pymatgen.analysis.quasirrho import QuasiRRHO, get_avg_mom_inertia
8+
from pymatgen.io.gaussian import GaussianOutput
9+
from pymatgen.io.qchem.outputs import QCOutput
10+
from pymatgen.util.testing import TEST_FILES_DIR
11+
12+
13+
class TestQuasiRRHO(unittest.TestCase):
14+
"""Test class for QuasiRRHO"""
15+
16+
def setUp(self):
17+
test_dir = TEST_FILES_DIR
18+
self.gout = GaussianOutput(f"{test_dir}/molecules/quasirrho_gaufreq.log")
19+
self.linear_gout = GaussianOutput(f"{test_dir}/molecules/co2.log.gz")
20+
self.qout = QCOutput(f"{test_dir}/molecules/new_qchem_files/Frequency_no_equal.qout")
21+
22+
def test_qrrho_gaussian(self):
23+
"""
24+
Testing from a Gaussian Output file. Correct values are taken from
25+
Trevor Seguin's original bash script.
26+
"""
27+
correct_g = -884.776886
28+
correct_stot = 141.584080
29+
qrrho = QuasiRRHO.from_gaussian_output(self.gout)
30+
assert correct_stot == pytest.approx(qrrho.entropy_quasiRRHO, 0.1), "Incorrect total entropy"
31+
assert correct_g == pytest.approx(qrrho.free_energy_quasiRRHO), "Incorrect Quasi-RRHO free energy"
32+
33+
def test_qrrho_qchem(self):
34+
"""
35+
Testing from a QChem output file. "Correct" values are from
36+
the version of QuasiRRHO that worked for Gaussian.
37+
initial run.
38+
"""
39+
correct_g = -428.60768184222934
40+
correct_stot = 103.41012732045324
41+
# HO total entropy from QChem = 106.521
42+
43+
qrrho = QuasiRRHO.from_qc_output(self.qout)
44+
assert correct_stot == pytest.approx(qrrho.entropy_quasiRRHO, 0.1), "Incorrect total entropy"
45+
assert correct_g == pytest.approx(qrrho.free_energy_quasiRRHO), "Incorrect Quasi-RRHO free energy"
46+
47+
def test_rrho_manual(self):
48+
"""
49+
Test manual input creation. Values from GaussianOutput
50+
"""
51+
e = self.gout.final_energy
52+
mol = self.gout.final_structure
53+
vib_freqs = [f["frequency"] for f in self.gout.frequencies[-1]]
54+
55+
correct_g = -884.776886
56+
correct_stot = 141.584080
57+
qrrho = QuasiRRHO(mol=mol, energy=e, frequencies=vib_freqs, mult=1)
58+
assert correct_stot == pytest.approx(qrrho.entropy_quasiRRHO, 0.1), "Incorrect total entropy"
59+
assert correct_g == pytest.approx(qrrho.free_energy_quasiRRHO), "Incorrect Quasi-RRHO free energy"
60+
61+
def test_rrho_linear(self):
62+
"""Test on a linear CO2 molecule from Gaussian Output file.
63+
Correct free_energy_ho is checked with Gaussian's internal calculation.
64+
Correct free_energy_quasirrho is compared internally in the hope of
65+
preventing future errors.
66+
"""
67+
correct_g_ho = -187.642070
68+
correct_g_qrrho = -187.642725
69+
qrrho = QuasiRRHO.from_gaussian_output(self.linear_gout)
70+
assert correct_g_ho == pytest.approx(
71+
qrrho.free_energy_ho, rel=1e-5
72+
), f"Incorrect harmonic oscillator free energy, {correct_g_ho} != {qrrho.free_energy_ho}"
73+
assert correct_g_qrrho == pytest.approx(qrrho.free_energy_quasiRRHO), "Incorrect Quasi-RRHO free energy"
74+
75+
def test_extreme_temperature_and_pressure(self):
76+
qrrho = QuasiRRHO.from_gaussian_output(self.gout, temp=0.1, press=1e9)
77+
assert qrrho.temp == 0.1
78+
assert qrrho.press == 1e9
79+
80+
def test_get_avg_mom_inertia(self):
81+
mol = self.gout.final_structure
82+
avg_mom_inertia, inertia_eigen_vals = get_avg_mom_inertia(mol)
83+
assert avg_mom_inertia == pytest.approx(0)
84+
assert inertia_eigen_vals == pytest.approx([0, 0, 0])

tests/files/molecules/co2.log.gz

21.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)