8. Chemical Physics – Working with TAMkin¶
8.1. Macroscopic properties of molecular systems¶
TAMkin describes the partition function of molecular systems starting from the the ideal gas and the harmonic oscillator approximation. Once the partition function is specified, all thermodynamic quantities can be derived as a function of temperature and pressure (or density). Several extensions to the ideal gas and harmonic oscillator approximation are supported to improve the agreement between computational predictions and experimental observations.
8.1.1. Definition of the partition function¶
In order to compute thermodynamic quantities with TAMkin, it is crucial to specify the correct parameters for the partition function of the molecular system under scrutiny. One must pay special attention to the compatibility of the normal mode analysis (NMA, to compute the vibrational spectrum) and the definition of the partition function. TAMkin will not complain about unusual combinations of NMA’s and Partition functions.
In the tutorial below, all the microscopic computations are all carried out with
Gaussian03, but one can replace the load_molecule_...
line with anything
suitable to load data from other (quantum) chemistry packages. The same
instructions also work starting from Gaussian09 computations.
The text below shows snippets of Python code to perform certain tasks. When
you use these snippets in a TAMkin script, do not forget to add the line
from tamkin import *
on top of the file.
8.1.1.1. Ideal gas molecules¶
The following three lines of Python code construct a partition function for an ideal gas molecule using the standard options:
mol = load_molecule_g03fchk("gaussian.fchk")
nma = NMA(mol, ConstrainExt())
pf = PartFun(nma, [ExtTrans(), ExtRot()])
In this example, the file "gaussian.fchk"
comes from a Gaussian frequency
computation on the optimized geometry of the molecule of interest.
The following options can be used to specify the details, but the standard values for these options are suitable for most applications.
Options for ConstrainExt. See
tamkin.nma.ConstrainExt
for the details. TheConstrainExt()
object specifies that the normal mode analysis must be performed in 3N-6 (or 3N-5) internal coordinates. This is the default procedure in most programs.The method only gives the correct result when the geometry is sufficiently optimized, i.e. when the gradient of the energy is approximately zero. This condition will be checked by comparing the maximum absolute value of the gradient vector with a threshold value. If the threshold is violated, TAMkin raises an error and your script will abort. The threshold can be specified as an option to
ConstrainExt
. The following would relax the default threshold:nma = NMA(mol, ConstrainExt(gradient_threshold=0.01))
The method also checks if the molecule is linear, which is based on the angular moments of inertia. Any angular momentum below the parameter im_threshold is treated as zero. The following setting would allow more deviations from linearity in linear molecules:
nma = NMA(mol, ConstrainExt(im_threshold=10.0))
Note: that the im_threshold value is given here in internal (atomic) units. For comparison, the lowest moment of inertia in ethane is about 40435 atomic units.
One can always check the log file (see below) of a partition function to see if the molecule was considered to be linear or not. When combining both options, they must be separated by a comma:
nma = NMA(mol, ConstrainExt(gradient_threshold=0.01, im_threshold=10.0))
Full instead of ConstrainExt. See
tamkin.nma.Full
for the details. Instead of constraining the external degrees of freedom, one may also perform the normal mode analysis in 3N coordinates and hope that the Hessian is accurate enough to produce 6 zero frequencies. They will be elminated automatically for the vibrational contribution to the partition function.For a proper identification of the zero frequencies, the
Full
treatment will also check the linearity of the molecule, just like theConstrainExt
treatment. One can relax the linearity check as follows:nma = NMA(mol, Full(im_threshold=10.0))
There are no other options for the full treatment.
Arguments for the PartFun object. See
tamkin.partf.PartFun
for the details. The partition function object is merely a definition of the partition function. It can be used to compute the numerical value of the partition function (and many other properties) at a given temperature and pressure.The contributions to the partition function must be specified when the object is created. By default, the vibrational and the electronic part are included. The first argument is a normal mode analysis object. It is used to set up the vibrational and the electronic contribution. Additional contributions are given in a list (square brackets). For gas phase molecules, one typically adds the
ExtTrans
andExtRot
contributions.Options for ExtTrans. See
tamkin.partf.ExtTrans
for the details. TheExtTrans
class defines the translational contribution to the partition function. For practical reasons, this contribution also includes the many body effects of the partition function of the classical (ideal) gas, and the optional contributions for the constant pressure ensemble. The following options can be used:cp
(defaultcp=True
).By default the translational contribution includes the corrections to describe an NpT partition function instead of an NVT partition function. If you are interested in the constant volume boundary conditions (NVY ensemble), use the option
cp=False
.pressure
(defaultpressure=None
, only allowed whencp==True
).The default value of
pressure
is 1 bar for 3D gases, 75.64 mNewton/m for 2D gases (surface tension of water) and 1.0 atomic units for any other dimension. Note: several quantities derived from the partition function do not explicitly depend on the pressure in the case of ideal gases. If you want to see the (absence of) pressure dependence, use the methodExtTrans.set_pressure()
and recompute the thermodynamic quantities afterwards.density
(defaultdensity=None
, only allowed whencp==False
).The default value of
density
is 1 mol/mdim, where dim is the dimension of the gas. Note: several quantities derived from the partition function do not explicitly depend on the density in the case of ideal gases. If you want to see the (absence of) density dependence, use the methodExtTrans.set_density()
and recompute the thermodynamic quantities afterwards.dim
(defaultdim=3
).The dimension of the gas. For ordinary gases, the dimension is three.
mobile
(defaultmobile=None
).One may specify that only a part of the system has translational freedom. This is not relevant for molecules in the gas phase.
Options for ExtRot. See
tamkin.partf.ExtRot
for the details.symmetry_number
(defaultsymmetry_number=None
).When the symmetry number is not given, it is computed from the molecular geometry and topology. This may not work properly or very slowly for gigantic systems. In that case, specify symmetry_number=1, or whatever the number it should be.
im_threshold
(defaultim_threshold=1.0
).The threshold to determine if the molecule is linear or not. If one of the moments of inertia drops below this number, the molecule is considered to be linear. The value 1.0 is in internal (atomic) units.
Options for Vibrations. See
tamkin.partf.Vibrations
for the details. One does not have to add aVibrations
object to the list of contributions in the partition function, unless one wants to modify the options of the vibrational contribution.classical
(defaultclassical=False
)When True, the vibrations are treated classically. The QM treatment is the default. This may be useful to compare TAMkin results with Monte Carlo or Molecular Dynamics simulations.
freq_scaling
(defaultfreq_scaling=1.0
)Scaling factor for the classical part of the vibrational partition function, ie. excluding the zero-point term.
zp_scaling
(defaultzp_scaling=1.0
)Scaling factor for the zero-point term in the vibrational contribution [default=1]
8.1.1.2. Immobile adsorbed molecules¶
Make sure you first read and understand the section on partition functions for ideal gas molecules.
In this section, we show how one defines a partition function for a particle that is adsorbed on a surface (flat or inside a porous material) such that it can not rotate or translate over the surface once adsorbed. It can only adsorb at another site, if it first desorbs from the surface.
We assume that the adsorption energy is computed with Gaussian using a cluster approximation for the surface. This means that the cluster is terminated and that the atoms at the termination are fixed in space with constraints during the geometry optimization.
The following code can be used to define the partition function for such a system:
fixed = [0, 1, 2, ...] # atom indexes of the fixed atoms, counting from zero
mol_both = load_molecule_g03fchk("gaussian_both.fchk")
nma_both = NMA(mol_both, PHVA(fixed))
pf_both = PartFun(nma_both, [])
Compared to the gas phase, external translation and rotation are removed. The
file "gaussian_both.fchk"
comes from a frequency computation of the adsorbed
molecule on the cluster model of the surface. The list fixed
contains all
atom indexes that fixed in space during the optimization.
The partition function of the surface without absorbed species is defined as follows:
fixed = [0, 1, 2, ...] # atom indexes of the fixed atoms, counting from zero
mol_surf = load_molecule_g03fchk("gaussian_surf.fchk")
nma_surf = NMA(mol_surf, PHVA(fixed))
pf_surf = PartFun(nma_surf, [])
The surface is treated as a cluster fixed in space, i.e. there are no external
rotation and translation contributions to the partition function. The file
"gaussian_surf.fchk"
comes from a frequency computation on the surface
cluster model. The geometry of the cluster must be optimized with constraints on
the atoms that terminate the cluster. The list fixed
contains all
atom indexes that fixed in space during the optimization.
One may load the indexes of the fixed atoms from a Gaussian .com
file as
follows:
fixed = load_fixed_g03com("gaussian.com")
Be aware that the fixed atom indexes may be different in the two computations, but we recommend some consistency in this context. The following convention avoids a lot of confusion: put all your surface atoms in the beginning of the geometry definition, and within this group of surface atoms, put all fixed atoms first, then the free atoms.
8.1.1.3. Mobile adsorbed molecules¶
Make sure you first read and understand the section on partition functions for ideal gas molecules.
In this section, we show how one defines a partition function for a particle that is adsorbed on a surface. We assume that the particle can still hover over the surface and that this translational motion can be modeled with a 2D ideal gas partition function with a constant surface area. We also assume that the adsorbed molecule is free to rotate as it can do in the gas phase.
Further we assume that the adsorption energy is computed with Gaussian using a cluster approximation for the surface. This means that the cluster is terminated and that the atoms at the termination are fixed in space with constraints during the geometry optimization.
The following code can be used to define the partition function for such a system:
fixed = [0, 1, 2, ...] # atom indexes of the fixed atoms, counting from zero
mobile = [5, 6, 7, ...] # atom indexes of the mobile atoms, counting from zero
mol_both = load_molecule_g03fchk("gaussian_both.fchk")
nma_both = NMA(mol_both, PHVA(fixed))
pf_both = PartFun(nma_both, [ExtTrans(cp=False, dim=2, mobile=mobile), ExtRot()])
In this code, the file "gaussian_both.fchk"
comes from a frequency
computation of the adsorbed molecule on the cluster model of the surface. The
list fixed
contains all atom indexes that fixed in space during the
optimization. The mobile
atoms refer to those of the adsorbed species that
has 2D translational motion on the surface.
The partition function of the surface without the adsorbed molecule is constructed as follows:
fixed = [0, 1, 2, ...] # atom indexes of the fixed atoms, counting from zero
mol_surf = load_molecule_g03fchk("gaussian_surf.fchk")
nma_surf = NMA(mol_surf, PHVA(fixed))
pf_surf = PartFun(nma_surf, [])
The surface is treated as a cluster fixed in space, i.e. there are not external
rotation and translation contributions to its partition function. The file
"gaussian_surf.fchk"
comes from a frequency computation on the surface
cluster model. The geometry of the cluster must be optimized with constraints on
the atoms that terminate the cluster. The list fixed
contains all atom
indexes that fixed in space during the optimization.
One may load the indexes of the fixed atoms from a Gaussian .com
file as
follows:
fixed = load_fixed_g03com("gaussian.com")
Be aware that the fixed atom indexes may be different in the two computations, but we recommend some consistency in this context. The following convention avoids a lot of confusion: put all your surface atoms in the beginning of the geometry definition, and within this group of atoms, put all fixed atoms first, then the free atoms.
8.1.1.4. Free or hindered internal rotors¶
Make sure you first read and understand the section on partition functions for ideal gas molecules.
TODO
8.1.2. The Partition function dump file¶
Once a partition function is defined in your script, one can write an extensive description to a text file for later reference:
pf.write_to_file("partfun.txt")
It is recommended to double check the contents of the file.
8.1.3. Computation of thermodynamic quantities¶
Once a partition function object is created, one can start computing thermodynamic quantities at different temperatures and pressures (or densities).
8.1.3.1. Overview of standard quantities¶
Thermodynamic quantities can be computed for a given PartFun
object by
calling the appropriate methods. All extensive quantities, i.e. all quantities
except the chemical potential, are transformed into intensive quantities by
dividing through the number of particles. The following table relates the
methods to the meaning of the returned numbers for two common ensembles.
PartFun method |
Internal unit | NVT Ensemble (3D gas) | NpT Ensemble (3D gas) |
---|---|---|---|
internal_heat |
Hartree/particle | Internal energy (per particle) | Enthalpy (per particle) |
heat_capacity |
Hartree/(K*particle) | Heat capacity at constant volume (per particle) | Heat capacity at constant pressure (per particle) |
free_energy |
Hartree/particle | Helmholtz free energy (per particle) | Gibbs free energy (per particle) |
chemical_potential |
Hartree/particle | Chemical potential | (idem) |
entropy |
Hartree/particle | Entropy (per particle) | (idem) |
log |
1/particle | Logarithm of the partition function (per particle) | (idem) |
logt |
1/(K*particle) | First derivative of log towards temperature |
(idem) |
logtt |
1/(K^2*particle) | Second derivative of log towards temperature |
(idem) |
logn |
1/particle | Derivative of the logarithm of the partition function towards the number of particles | (idem) |
logv |
1/particle | logn - \(\ln(V/N)\) |
(idem) |
One can print any of these quantities in a TAMkin script:
from molmod import * # for the unit conversion
pf = ...
print "The internal heat at 300K [kJ/mol]", pf.internal_heat(300)/kjmol
print "The heat capacity at 300K [J/mol/K]", pf.heat_capacity(300)/(joule/(mol*kelvin))
8.1.3.2. Poking under the hood¶
Besides the standard thermodynamic functions, all internal quantities of the partition function and its contributions are also accessible. For example, one computes the translational contribution to the free energy as follows:
from molmod import * # for the unit conversion
pf = ...
print "The free energy at 300K due to translation [kJ/mol]", pf.translational.internal_heat(300)/kjmol
A complete overview of internals can be found in the reference documentation
of the tamkin.partf
module, or by reading the source code.
8.1.3.3. Generating tables¶
See tamkin.pftools.ThermoAnalysis
for the details.
Tables of thermodynamic quantities can be computed for given temperatures and sorting out all contributions from the components of the partition function to each quantity. The example below generates a CSV file that can be loaded into spreadsheet software.
from tamkin import *
molecule = load_molecule_g03fchk("gaussian.fchk")
nma = NMA(molecule, ConstrainExt())
pf = PartFun(nma, [ExtTrans(), ExtRot()])
ta = ThermoAnalysis(pf, [300, 400, 500, 600])
ta.write_to_file("thermo.csv")
The CSV file contains tables with thermodynamic quantities, at the temperatures
in the second argument of the ThermoAnalysis
constructor, corresponding to
the PartFun
methods as explained the table below.
Name in CSV file | Unit | PartFun method name |
---|---|---|
Internal Heat | kJ/mol | internal_heat |
Heat capacity | J/(mol*K) | heat_capacity |
Free energy | kJ/mol | free_energy |
Chemical potential | kJ/mol | chemical_potential |
Entropy | J/(mol*K) | entropy |
log= \(\frac{log(Z_N)}{N}\) | 1 | log |
logt= \(\frac{\partial}{\partial T}\left(\frac{log(Z_N)}{N}\right)\) | 1/K | logt |
logtt= \(\frac{\partial^2}{\partial T^2}\left(\frac{log(Z_N)}{N}\right)\) | 1/K^2 | logtt |
logn= \(\frac{\partial log(Z_N)}{\partial N}\) | 1 | logn |
logv= \(\frac{\partial log(Z_N)}{\partial N} - \ln(V/N)\) | 1 or ln(bohr^-dim) | logv |
8.2. Thermodynamic equilibrium¶
See tamkin.chemmod.ThermodynamicModel
for the details.
Given a list of partition functions of reactants (pfs_react
) and a list of
product partition functions (pfs_prod
), one can construct a
ThermodynamicModel
object.
tm = ThermodynamicModel(pfs_react, pfs_prod)
With a ThermodynamicModel
object one can compute the following quantities:
The equilibrium constant is computed at a given temperature,
temp
, as follows:kc = tm.equilibrium_constant(temp)
This function takes one optional argument:
do_log
, which is by defaultFalse
. When set to True, the logarithm of the equilibrium constant is returned. The name of the SI unit of the equilibrium constant and the corresponding conversion factor are attributes of theThermodymanicModel
object. This can be used to facilitate the output of equilibrium constants. For example:print "K_c at 350K [%s] = %.5e" % (tm.unit_name, kc/tm.unit)
Currently TAMkin only supports ideal gases in the translational partition function which means that \(K_c\) does not depend on the pressure set in
ExtTrans.pressure
or the density set inExtTrans.density
. (When no translational freedom is included in the partition function, there is no pressure or density to worry about.)The change in free energy is computed (and printed) as follows:
delta_fr = tm.free_energy_change(temp) print "Change in free energy [kJ/mol] = %.3f" % (delta_fr/kjmol)
The change in free energy does depend on the pressure (or density) in the translational part of the partition functions of the reactants and products.
The electronic energy difference between the reactants (-) and the products (+) can be computed as follows:
delta_e = tm.energy_difference(temp)
The zero-point energy difference between the reactants (-) and the products (+) can also be computed:
delta_zpe = tm.zero_point_energy_difference(temp)
A complete overview of the thermodynamic model, including the specification of the partition functions, is written to file as follows:
tm.write_to_file("thermodynamic_model.txt")
8.3. Reaction kinetics¶
See tamkin.chemmod.KineticModel
for the details.
All kinetic properties in TAMkin are computed with the KineticModel
object.
For a given list of reactant partition functions, pfs_react
and a transition
state partition function, pf_trans
, the KineticModel
object is created
as follows:
km = KineticModel(pfs_react, pf_trans)
The following kinetic properties can be computed with a KineticModel
object:
The rate constant is the main property of interest. The following two lines print the rate constant at 303K in SI units for a given kinetic model.
rc = km.rate_constant(303) print "Rate constant [%s] at 303K = %.5e" % (km.unit_name, rc/km.unit)
In the case of ideal gases, the rate constant does not depend on the pressure (or density) set in the translation partition functions.
The change in free energy when going from reactants to products is computed as follows for a given temperature
temp
:delta_fr = km.free_energy_change(temp)
The electronic energy difference between the reactants (-) and the transition state (+) is computed as follows:
delta_e = km.energy_difference(temp)
The zero-point energy difference between the reactants (-) and the transition state (+) can also be computed:
delta_zpe = km.zero_point_energy_difference(temp)
A complete overview of the kinetic model, including the specification of the partition functions, is written to file as follows:
km.write_to_file("kinetic_model.txt")
8.3.1. Tunneling corrections¶
Three tunneling correction models are implemented in tamkin.tunneling
.
One can include tunneling corrections in the kinetic model by giving a
TunnelingCorrection
object as the third argument to the KineticModel
constructor. One must first create the tunneling object:
# Just take one of the three following
tunneling = Eckart(pfs_react, pf_trans, pfs_prod)
tunneling = Wigner(pf_trans)
tunneling = Miller(pf_trans)
# Then create a kinetic model
km = KineticModel(pfs_react, pf_trans, tunneling)
In this snippet pfs_prod
is the list of product partition functions. All
rate constants computed with such a kinetic model include the tunneling
correction. The other properties computed by the kinetic model are not affected.
8.3.2. ReactionAnalysis objects – fitting kinetic parameters A and Ea¶
See tamkin.pftools.ReactionAnalysis
for the details.
The ultimate purpose of a KineticModel
object is to estimate the kinetic
parameters \(A\) and \(E_a\) in the empirical Arrhenius law. This can
be accomplished with a ReactionAnalysis object. As soon as a reaction analysis
object is created, the kinetic parameters are fitted and stored internally for
post-processing or direct output:
ra = ReactionAnalysis(km, temp_low, temp_high)
The first argument is a kinetic model, which may include a tunneling correction. The two following arguments are the boundaries for the grid on the temperature axis used to fit the kinetic parameters. A fourth optional argument fixes the spacing of the temperature grid. The default spacing is 10K.
The kinetic parameters and some related data are accessible as attributes:
A
andEa
– The kinetic parameters in atomic units.R2
– The Pearson R^2 of the fit.temps
– An array with the temperature grid in Kelvinrate_consts
– the rate constants at the grid points in atomic unitsln_rate_consts
– the logarithm of the rate constants in atomic units
The pre-exponential factor can be printed in SI units, knowing that it has the same unit as the rate constant itself:
print "Pre-exponential factor [%s] = %.5e" % (km.unit_name, ra.A/km.unit)
print "The activation energy [kJ/mol] = %.2f" % (ra.Ea/kjmol)
When performing a high-throughput level-of-theory study, it is very convenient
to use the attributes of the ReactionAnalysis
in a post-processing program.
One can write an overview of the reaction analysis to a text file as follows:
ra.write_to_file("reaction_analysis.txt")
The computed rate constants and the linear fit can be plotted in a typical Arrhenius plot as follows:
ra.plot_arrhenius("arrhenius.txt")