#!/usr/bin/env python
# coding: utf-8
# # Overview of pMuTT's Core Functionality
# Originally written for Version 1.2.1
#
# Last Updated for Version 1.2.13
#
# ## Topics Covered
#
# - Using constants and converting units using the ``constants`` module
# - Initializing ``StatMech`` objects by specifying all modes and by using ``presets`` dictionary
# - Initializing empirical objects such as ``Nasa`` objects using a ``StatMech`` object or from a previously generated Nasa polynomial
# - Initializing ``Reference`` and ``References`` objects to adjust DFT's reference to more traditional references
# - Input (via Excel) and output ``Nasa`` polynomials to thermdat format
# - Initializing ``Reaction`` objects from strings
#
# ## Useful Links:
#
# - Github: https://github.com/VlachosGroup/pMuTT
# - Documentation: https://vlachosgroup.github.io/pMuTT/index.html
# - Examples: https://vlachosgroup.github.io/pMuTT/examples.html
# ## Constants
# pMuTT has a wide variety of constants to increase readability of the code. See [Constants page][0] in the documentation for supported units.
#
# [0]: https://vlachosgroup.github.io/pmutt/constants.html#constants
# In[1]:
from pmutt import constants as c
1.987
print(c.R('kJ/mol/K'))
print('Some constants')
print('R (J/mol/K) = {}'.format(c.R('J/mol/K')))
print("Avogadro's number = {}\n".format(c.Na))
print('Unit conversions')
print('5 kJ/mol --> {} eV/molecule'.format(c.convert_unit(num=5., initial='kJ/mol', final='eV/molecule')))
print('Frequency of 1000 Hz --> Wavenumber of {} 1/cm\n'.format(c.freq_to_wavenumber(1000.)))
print('See expected inputs, supported units of different constants')
help(c.R)
help(c.convert_unit)
# ## StatMech Objects
# Molecules show translational, vibrational, rotational, electronic, and nuclear modes.
#
#
#
# The [``StatMech``][0] object allows us to specify translational, vibrational, rotational, electronic and nuclear modes independently, which gives flexibility in what behavior you would like.
#
# [0]: https://vlachosgroup.github.io/pmutt/statmech.html#pmutt.statmech.StatMech
# For this example, we will use a butane molecule as an ideal gas:
#
# - translations with no interaction between molecules
# - harmonic vibrations
# - rigid rotor rotations
# - ground state electronic structure -
# - ground state nuclear structure).
#
#
# In[2]:
from ase.build import molecule
from pmutt.statmech import StatMech, trans, vib, rot, elec
butane_atoms = molecule('trans-butane')
'''Translational'''
butane_trans = trans.FreeTrans(n_degrees=3, atoms=butane_atoms)
'''Vibrational'''
butane_vib = vib.HarmonicVib(vib_wavenumbers=[3054.622862, 3047.573455, 3037.53448,
3030.21322, 3029.947329, 2995.758708,
2970.12166, 2968.142985, 2951.122942,
2871.560685, 1491.354921, 1456.480829,
1455.224163, 1429.084081, 1423.153673,
1364.456094, 1349.778994, 1321.137752,
1297.412109, 1276.969173, 1267.783512,
1150.401492, 1027.841298, 1018.203753,
945.310074, 929.15992, 911.661049,
808.685354, 730.986587, 475.287654,
339.164649, 264.682213, 244.584138,
219.956713, 115.923768, 35.56194])
'''Rotational'''
butane_rot = rot.RigidRotor(symmetrynumber=2, atoms=butane_atoms)
'''Electronic'''
butane_elec = elec.GroundStateElec(potentialenergy=-73.7051, spin=0)
'''StatMech Initialization'''
butane_statmech = StatMech(name='butane',
trans_model=butane_trans,
vib_model=butane_vib,
rot_model=butane_rot,
elec_model=butane_elec)
H_statmech = butane_statmech.get_H(T=298., units='kJ/mol')
S_statmech = butane_statmech.get_S(T=298., units='J/mol/K')
print('H_butane(T=298) = {:.1f} kJ/mol'.format(H_statmech))
print('S_butane(T=298) = {:.2f} J/mol/K'.format(S_statmech))
# ### Presets
# The [``presets``][0] dictionary stores commonly used models to ease the initialization of [``StatMech``][1] objects. The same water molecule before can be initialized this way instead.
#
# [0]: https://vlachosgroup.github.io/pmutt/statmech.html#presets
# [1]: https://vlachosgroup.github.io/pmutt/statmech.html#pmutt.statmech.StatMech
# In[3]:
from pprint import pprint
from ase.build import molecule
from pmutt.statmech import StatMech, presets
idealgas_defaults = presets['idealgas']
pprint(idealgas_defaults)
# In[4]:
butane_preset = StatMech(name='butane',
atoms=molecule('trans-butane'),
vib_wavenumbers=[3054.622862, 3047.573455, 3037.53448,
3030.21322, 3029.947329, 2995.758708,
2970.12166, 2968.142985, 2951.122942,
2871.560685, 1491.354921, 1456.480829,
1455.224163, 1429.084081, 1423.153673,
1364.456094, 1349.778994, 1321.137752,
1297.412109, 1276.969173, 1267.783512,
1150.401492, 1027.841298, 1018.203753,
945.310074, 929.15992, 911.661049,
808.685354, 730.986587, 475.287654,
339.164649, 264.682213, 244.584138,
219.956713, 115.923768, 35.56194],
symmetrynumber=2,
potentialenergy=-73.7051,
spin=0,
**idealgas_defaults)
H_preset = butane_preset.get_H(T=298., units='kJ/mol')
S_preset = butane_preset.get_S(T=298., units='J/mol/K')
print('H_butane(T=298) = {:.1f} kJ/mol'.format(H_preset))
print('S_butane(T=298) = {:.2f} J/mol/K'.format(S_preset))
# ### Empty Modes
# The [``EmptyMode``][0] is a special object returns 1 for the partition function and 0 for all other thermodynamic properties. This is useful if you do not want any contribution from a mode.
#
# [0]: https://vlachosgroup.github.io/pmutt/statmech.html#empty-mode
# In[5]:
from pmutt.statmech import EmptyMode
empty = EmptyMode()
print('Some EmptyMode properties:')
print('q = {}'.format(empty.get_q()))
print('H/RT = {}'.format(empty.get_HoRT()))
print('S/R = {}'.format(empty.get_SoR()))
print('G/RT = {}'.format(empty.get_GoRT()))
# ## Empirical Objects
# Empirical forms are polynomials that are fit to experimental or *ab-initio data*. These forms are useful because they can be evaluated relatively quickly, so that downstream software is not hindered by evaluation of thermochemical properties.
# However, note that ``StatMech`` objects can calculate more properties than the currently supported empirical objects.
# ### NASA polynomial
# The [``NASA``][0] format is used for our microkinetic modeling software, Chemkin.
#
# #### Initializing Nasa from StatMech
# Below, we initialize the NASA polynomial from the ``StatMech`` object we created earlier.
#
# [0]: https://vlachosgroup.github.io/pmutt/empirical.html#nasa
# In[6]:
from pmutt.empirical.nasa import Nasa
butane_nasa = Nasa.from_model(name='butane',
model=butane_statmech,
T_low=298.,
T_high=800.,
elements={'C': 4, 'H': 10},
phase='G')
H_nasa = butane_nasa.get_H(T=298., units='kJ/mol')
S_nasa = butane_nasa.get_S(T=298., units='J/mol/K')
print('H_butane(T=298) = {:.1f} kJ/mol'.format(H_nasa))
print('S_butane(T=298) = {:.2f} J/mol/K'.format(S_nasa))
# Although it is not covered here, you can also generate empirical objects from experimental data using the ``.from_data`` method. See [Experimental to Empirical][6] example.
#
# [6]: https://vlachosgroup.github.io/pmutt/examples.html#experimental-to-empirical
# #### Initializing Nasa Directly
# We can also initialize the NASA polynomial if we have the polynomials. Using an entry from the [Reaction Mechanism Generator (RMG) database][0].
#
# [0]: https://rmg.mit.edu/database/thermo/libraries/DFT_QCI_thermo/215/
# In[7]:
import numpy as np
butane_nasa_direct = Nasa(name='butane',
T_low=100.,
T_mid=1147.61,
T_high=5000.,
a_low=np.array([ 2.16917742E+00,
3.43905384E-02,
-1.61588593E-06,
-1.30723691E-08,
5.17339469E-12,
-1.72505133E+04,
1.46546944E+01]),
a_high=np.array([ 6.78908025E+00,
3.03597365E-02,
-1.21261608E-05,
2.19944009E-09,
-1.50288488E-13,
-1.91058191E+04,
-1.17331911E+01]),
elements={'C': 4, 'H': 10},
phase='G')
H_nasa_direct = butane_nasa_direct.get_H(T=298., units='kJ/mol')
S_nasa_direct = butane_nasa_direct.get_S(T=298., units='J/mol/K')
print('H_butane(T=298) = {:.1f} kJ/mol'.format(H_nasa_direct))
print('S_butane(T=298) = {:.2f} J/mol/K'.format(S_nasa_direct))
# Compare the results between ``butane_nasa`` and ``butane_nasa_direct`` to the [Wikipedia page for butane][0].
#
# [0]: https://en.wikipedia.org/wiki/Butane_(data_page)
# In[8]:
print('H_nasa = {:.1f} kJ/mol'.format(H_nasa))
print('H_nasa_direct = {:.1f} kJ/mol'.format(H_nasa_direct))
print('H_wiki = -125.6 kJ/mol\n')
print('S_nasa = {:.2f} J/mol/K'.format(S_nasa))
print('S_nasa_direct = {:.2f} J/mol/K'.format(S_nasa_direct))
print('S_wiki = 310.23 J/mol/K')
# Notice the values are very different for ``H_nasa``. This discrepancy is due to:
#
# - different references
# - error in DFT
#
# We can account for this discrepancy by using the [``Reference``][0] and [``References``][1] objects.
#
# [0]: https://vlachosgroup.github.io/pmutt/empirical.html#pmutt.empirical.references.Reference
# [1]: https://vlachosgroup.github.io/pmutt/empirical.html#pmutt.empirical.references.References
# ### Referencing
# To define a reference, you must have:
#
# - enthalpy at some reference temperature (``HoRT_ref`` and ``T_ref``)
# - a ``StatMech`` object
#
# In general, use references that are similar to molecules in your mechanism. Also, the number of reference molecules must be equation to the number of elements (or other descriptor) in the mechanism. [Full description of referencing scheme here][0].
#
# In this example, we will use ethane and propane as references.
#
#
#
# [0]: https://vlachosgroup.github.io/pmutt/referencing.html
# In[9]:
from pmutt.empirical.references import Reference, References
ethane_ref = Reference(name='ethane',
elements={'C': 2, 'H': 6},
atoms=molecule('C2H6'),
vib_wavenumbers=[3050.5296, 3049.8428, 3025.2714,
3024.4304, 2973.5455, 2971.9261,
1455.4203, 1454.9941, 1454.2055,
1453.7038, 1372.4786, 1358.3593,
1176.4512, 1175.507, 992.55,
803.082, 801.4536, 298.4712],
symmetrynumber=6,
potentialenergy=-40.5194,
spin=0,
T_ref=298.15,
HoRT_ref=-33.7596,
**idealgas_defaults)
propane_ref = Reference(name='propane',
elements={'C': 3, 'H': 8},
atoms=molecule('C3H8'),
vib_wavenumbers=[3040.9733, 3038.992, 3036.8071,
3027.6062, 2984.8436, 2966.1692,
2963.4684, 2959.7431, 1462.5683,
1457.4221, 1446.858, 1442.0357,
1438.7871, 1369.6901, 1352.6287,
1316.215, 1273.9426, 1170.4456,
1140.9699, 1049.3866, 902.8507,
885.3209, 865.5958, 735.1924,
362.7372, 266.3928, 221.4547],
symmetrynumber=2,
potentialenergy=-57.0864,
spin=0,
T_ref=298.15,
HoRT_ref=-42.2333,
**idealgas_defaults)
refs = References(references=[ethane_ref, propane_ref])
print(refs.offset)
# Passing the ``References`` object when we make our ``Nasa`` object produces a value closer to the one listed above.
# In[10]:
butane_nasa_ref = Nasa.from_model(name='butane',
model=butane_statmech,
T_low=298.,
T_high=800.,
elements={'C': 4, 'H': 10},
references=refs)
H_nasa_ref = butane_nasa_ref.get_H(T=298., units='kJ/mol')
S_nasa_ref = butane_nasa_ref.get_S(T=298., units='J/mol/K')
print('H_butane(T=298) = {:.1f} kJ/mol'.format(H_nasa_ref))
print('S_butane(T=298) = {:.2f} J/mol/K'.format(S_nasa_ref))
# ## Input and Output
# ### Excel
# Encoding each object in Python can be tedious and so you can read from Excel spreadsheets using [``pmutt.io.excel.read_excel``][0]. Note that this function returns a list of dictionaries. This output allows you to initialize whichever object you want. There are also special rules that depend on the header name.
#
# [0]: https://vlachosgroup.github.io/pmutt/io.html?highlight=read_excel#pmutt.io.excel.read_excel
# In[11]:
import os
from pathlib import Path
from pmutt.io.excel import read_excel
# Find the location of Jupyter notebook
# Note that normally Python scripts have a __file__ variable but Jupyter notebook doesn't.
# Using pathlib can overcome this limiation
try:
notebook_folder = os.path.dirname(__file__)
except NameError:
notebook_folder = Path().resolve()
os.chdir(notebook_folder)
# The Excel spreadsheet is located in the same folder as the Jupyter notebook
refs_path = os.path.join(notebook_folder, 'refs.xlsx')
refs_data = read_excel(refs_path)
pprint(refs_data)
# Initialize using \*\*kwargs syntax.
# In[12]:
ref_list = []
for record in refs_data:
ref_list.append(Reference(**record))
refs_excel = References(references=ref_list)
print(refs_excel.offset)
# Butane can be initialized in a similar way.
# In[13]:
# The Excel spreadsheet is located in the same folder as the Jupyter notebook
butane_path = os.path.join(notebook_folder, 'butane.xlsx')
butane_data = read_excel(butane_path)[0] # [0] accesses the butane data
butane_excel = Nasa.from_model(T_low=298.,
T_high=800.,
references=refs_excel,
**butane_data)
H_excel = butane_excel.get_H(T=298., units='kJ/mol')
S_excel = butane_excel.get_S(T=298., units='J/mol/K')
print('H_butane(T=298) = {:.1f} kJ/mol'.format(H_excel))
print('S_butane(T=298) = {:.2f} J/mol/K'.format(S_excel))
# ### Thermdat
# The thermdat format uses NASA polynomials to represent several species. It has a very particular format so doing it manually is error-prone. You can write a list of ``Nasa`` objects to thermdat format using [``pmutt.io.thermdat.write_thermdat``][0].
#
# [0]: https://vlachosgroup.github.io/pmutt/io.html#pmutt.io.thermdat.write_thermdat
# In[14]:
from pmutt.io.thermdat import write_thermdat
# Make Nasa objects from previously defined ethane and propane
ethane_nasa = Nasa.from_model(name='ethane',
phase='G',
T_low=298.,
T_high=800.,
model=ethane_ref.model,
elements=ethane_ref.elements,
references=refs)
propane_nasa = Nasa.from_model(name='propane',
phase='G',
T_low=298.,
T_high=800.,
model=propane_ref.model,
elements=propane_ref.elements,
references=refs)
nasa_species = [ethane_nasa, propane_nasa, butane_nasa]
# Determine the output path and write the thermdat file
thermdat_path = os.path.join(notebook_folder, 'thermdat')
write_thermdat(filename=thermdat_path, nasa_species=nasa_species)
THERMO ALL
100 500 1500
ethane 20190122C 2H 6 G298.0 800.0 502.9 1
-6.84729317E-01 2.54150584E-02-1.02900193E-05-1.15304090E-09 1.73800327E-12 2
-1.08866341E+04 2.42655635E+01 6.77655563E+00-3.31037518E-02 1.63456769E-04 3
-2.32569163E-07 1.18361103E-10-1.16548936E+04-6.74224205E+00 4
propane 20190122C 3H 8 G298.0 800.0 502.9 1
-2.54716915E+00 4.36640749E-02-2.65978975E-05 6.89956646E-09 7.68567634E-14 2
-1.35353831E+04 3.50227651E+01 8.09253651E+00-4.01657054E-02 2.23439464E-04 3
-3.27632587E-07 1.69405578E-10-1.46259783E+04-9.14554121E+00 4
butane 20190122C 4H 10 G298.0 800.0 502.9 1
-1.98482876E+00 5.99910769E-02-4.67983727E-05 2.14853318E-08-4.31005098E-12 2
-8.15415828E+05 3.48691129E+01 9.98107805E+00-3.50556935E-02 2.38938732E-04 3
-3.63709779E-07 1.92062158E-10-8.16632302E+05-1.47065846E+01 4
END
# Similarly, [``pmutt.io.thermdat.read_thermdat``][0] reads thermdat files.
#
# [0]: https://vlachosgroup.github.io/pmutt/io.html#pmutt.io.thermdat.read_thermdat
# ## Reactions
# You can also evaluate reactions properties. The most straightforward way to do this is to initialize using strings.
# In[15]:
from pmutt.io.thermdat import read_thermdat
from pmutt import pmutt_list_to_dict
from pmutt.reaction import Reaction
# Get a dictionary of species
thermdat_H2O_path = os.path.join(notebook_folder, 'thermdat_H2O')
species_list = read_thermdat(thermdat_H2O_path)
species_dict = pmutt_list_to_dict(species_list)
# Initialize the reaction
rxn_H2O = Reaction.from_string('H2 + 0.5O2 = H2O', species=species_dict)
# Calculate reaction properties
H_rxn = rxn_H2O.get_delta_H(T=298., units='kJ/mol')
S_rxn = rxn_H2O.get_delta_S(T=298., units='J/mol/K')
print('H_rxn(T=298) = {:.1f} kJ/mol'.format(H_rxn))
print('S_rxn(T=298) = {:.2f} J/mol/K'.format(S_rxn))
# ## Exercise
# Write a script to calculate the Enthalpy of adsorption (in kcal/mol) of H2O on Cu(111) at T = 298 K. Some important details are given below.
#
# ### Information Required
# #### H2O:
# - ideal gas
# - atoms: You can use "ase.build.molecule" to generate a water molecule like we did with ethane, propane, and butane.
# - vibrational wavenumbers (1/cm): 3825.434, 3710.2642, 1582.432
# - potential energy (eV): -14.22393533
# - spin: 0
# - symmetry number: 2
#
# #### Cu(111):
# - only electronic modes
# - potential energy (eV): -224.13045381
#
# #### H2O+Cu(111):
# - electronic and harmonic vibration modes
# - potential energy (eV): -238.4713854
# - vibrational wavenumbers (1/cm): 3797.255519, 3658.895695, 1530.600295, 266.366130, 138.907356, 63.899768, 59.150454, 51.256019, -327.384554 (negative numbers represent imaginary frequencies. The default behavior of pMuTT is to ignore these frequencies when calculating any thermodynamic property)
#
# #### Reaction:
# H2O + Cu(111) --> H2O+Cu(111)
#
# ### Solution:
# In[16]:
from ase.build import molecule
from pmutt.statmech import StatMech, presets
from pmutt.reaction import Reaction
# Using dictionary since later I will initialize the reaction with a string
species = {
'H2O(g)': StatMech(atoms=molecule('H2O'),
vib_wavenumbers=[3825.434, 3710.2642, 1582.432],
potentialenergy=-14.22393533,
spin=0,
symmetrynumber=2,
**presets['idealgas']),
'*': StatMech(potentialenergy=-224.13045381,
**presets['electronic']),
'H2O*': StatMech(potentialenergy=-238.4713854,
vib_wavenumbers=[3797.255519,
3658.895695,
1530.600295,
266.366130,
138.907356,
63.899768,
59.150454,
51.256019,
-327.384554], #Imaginary frequency!
**presets['harmonic']),
}
rxn = Reaction.from_string('H2O(g) + * = H2O*', species)
del_H = rxn.get_delta_H(T=298., units='kcal/mol')
print('del_H = {:.2f} kcal/mol'.format(del_H))
# In[ ]: