Skip to content

Commit c8a76b5

Browse files
committed
Merge pull request #5 from adelq/JANAF
JANAF
2 parents f05bd80 + 935c7de commit c8a76b5

9 files changed

Lines changed: 2069 additions & 2 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,8 @@ docs/_build/
5757
# PyBuilder
5858
target/
5959

60+
# PyCharm
61+
.idea/
62+
63+
# JANAF cache directory
64+
thermochem/JANAF_Cache/

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ include TODO
66
include ez_setup.py
77

88
recursive-include thermochem *.xml
9+
recursive-include thermochem *.txt

README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ and Thermochemistry. The following tools are provided:
1212

1313
- Alexander Burcat's database and routines to compute the properties
1414
of more than 1300 substances from 200K to 6000K.
15+
16+
- JANAF thermodynamic data pulled from NIST website to compute
17+
properties of more than 1200 substances from 0K to 2500K.
1518

1619
- Simple reactor for modeling combustion of single organic compounds.
1720

@@ -28,6 +31,8 @@ This packages needs the following extra modules to run.
2831

2932
- Scipy >= 0.6.0
3033

34+
- Pandas >= 0.18.0
35+
3136
- setuptools >= 0.6
3237

3338
Development

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
license="BSD",
1111
packages=find_packages(),
1212
include_package_data=True,
13-
install_requires=['scipy>=0.6.0', 'numpy>=1.2.1'],
13+
install_requires=['scipy>=0.6.0', 'numpy>=1.2.1', 'pandas>=0.17.0'],
1414
zip_safe=False,
1515
keywords='thermo chemistry physics',
1616
classifiers=[

tests/test_janaf.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from thermochem.janaf import Janafdb
2+
from nose.tools import eq_
3+
from numpy.testing import assert_allclose
4+
5+
6+
def test_titanium():
7+
db = Janafdb()
8+
ti = db.search('Ti')
9+
eq_(len(ti), 88)
10+
11+
12+
def test_rutile():
13+
db = Janafdb()
14+
rutile = db.getphasedata(formula='O2Ti', name='Rutile', phase='cr')
15+
assert_allclose(rutile.cp([500, 550, 1800]), [67.203, 68.567, 78.283])

thermochem/JANAF_index.txt

Lines changed: 1796 additions & 0 deletions
Large diffs are not rendered by default.

thermochem/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.5.1'
1+
__version__ = '0.6.0'

thermochem/janaf.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
"""
2+
This module gets thermodynamic datat from the JANAF database.
3+
Files are downloaded from the NIST servers as needed and then cached locally.
4+
5+
Zack Gainsforth
6+
7+
Funding by NASA
8+
"""
9+
10+
from __future__ import division
11+
from __future__ import print_function
12+
13+
import numpy as np
14+
import pandas as pd
15+
from scipy.interpolate import interp1d
16+
import os
17+
18+
try:
19+
# Python 3
20+
import urllib.request as urllib2
21+
except ImportError:
22+
# Python 2
23+
import urllib2
24+
25+
try:
26+
# Python 2
27+
from StringIO import StringIO
28+
except ImportError:
29+
# Python 3
30+
from io import StringIO
31+
32+
# Universal gas constant R
33+
R = 8.314472
34+
35+
36+
class JanafPhase(object):
37+
"""
38+
Class which is created by Janafdb for a specific phase.
39+
40+
It reads in the JANAF data file and produces functions which interpolate
41+
the thermodynamic constants.
42+
43+
>>> db = Janafdb()
44+
>>> p = db.getphasedata(formula='O2Ti', name='Rutile', phase='cr')
45+
>>> print(p.cp([500, 550, 1800]))
46+
[ 67.203 68.567 78.283]
47+
>>> print(p.S([500, 550, 1800]))
48+
[ 82.201 88.4565 176.876 ]
49+
>>> print(p.gef([500, 550, 1800]))
50+
[ 57.077 59.704 115.753]
51+
>>> print(p.hef([500, 550, 1800]))
52+
[ 12.562 15.9955 110.022 ]
53+
>>> print(p.DeltaH([500, 550, 1800]))
54+
[-943670. -943229.5 -936679. ]
55+
>>> print(p.DeltaG([500, 550, 1800]))
56+
[-852157. -843046.5 -621013. ]
57+
>>> print(p.logKf([500, 550, 1800]))
58+
[ 89.024 80.8125 18.021 ]
59+
>>> print(p.cp(50000))
60+
Traceback (most recent call last):
61+
...
62+
ValueError: A value in x_new is above the interpolation range.
63+
"""
64+
65+
def __init__(self, rawdata_text):
66+
# Store the raw data text file from NIST.
67+
self.rawdata_text = rawdata_text
68+
69+
# Read the text file into a DataFrame.
70+
data = pd.read_csv(
71+
StringIO(self.rawdata_text),
72+
skiprows=2,
73+
header=None,
74+
delimiter='[\t\s]*',
75+
engine='python')
76+
data.columns = ['T', 'Cp', 'S', '[G-H(Tr)]/T', 'H-H(Tr)', 'Delta_fH',
77+
'Delta_fG', 'log(Kf)']
78+
self.rawdata = data
79+
80+
# Sometimes the JANAF files have funky stuff written in them. (Old
81+
# school text format...)
82+
# Clean it up.
83+
for c in data.columns:
84+
# We only need to polish up columns that aren't floating point
85+
# numbers.
86+
if np.issubdtype(data.dtypes[c], np.floating):
87+
continue
88+
# Change INFINITE to inf
89+
data.loc[data[c] == 'INFINITE', c]
90+
# Anything else becomes a nan.
91+
# Convert to floats.
92+
data[c] = pd.to_numeric(data[c], errors='coerce')
93+
94+
# Convert all units to Joules.
95+
data['Delta_fH'] *= 1000
96+
data['Delta_fG'] *= 1000
97+
98+
# Now make interpolatable functions for each of these.
99+
self.cp = interp1d(self.rawdata['T'], self.rawdata['Cp'])
100+
self.S = interp1d(self.rawdata['T'], self.rawdata['S'])
101+
self.gef = interp1d(self.rawdata['T'], self.rawdata['[G-H(Tr)]/T'])
102+
self.hef = interp1d(self.rawdata['T'], self.rawdata['H-H(Tr)'])
103+
self.DeltaH = interp1d(self.rawdata['T'], self.rawdata['Delta_fH'])
104+
self.DeltaG = interp1d(self.rawdata['T'], self.rawdata['Delta_fG'])
105+
self.logKf = interp1d(self.rawdata['T'], self.rawdata['log(Kf)'])
106+
107+
# TODO Deal well with crystal<->liquid transitions which have a below
108+
# and above value for Cp, S, etc.
109+
110+
111+
class Janafdb(object):
112+
"""
113+
Class that reads the NIST JANAF tables for thermodynamic data.
114+
115+
Data is initially read from the web servers, and then cached.
116+
"""
117+
VALIDPHASETYPES = ['cr', 'l', 'cr,l', 'g', 'ref', 'cd', 'fl', 'am', 'vit',
118+
'mon', 'pol', 'sln', 'aq', 'sat']
119+
JANAF_URL = "http://kinetics.nist.gov/janaf/html/%s.txt"
120+
121+
def __init__(self):
122+
"""
123+
We have an index file which can be used to build the url for all phases
124+
on the NIST site.
125+
"""
126+
127+
# Read the index file which tells us the filenames for all the phases
128+
# in the JANAF database.
129+
dirname = os.path.dirname(__file__)
130+
janaf_index = os.path.join(dirname, 'JANAF_index.txt')
131+
self.db = pd.read_csv(janaf_index, delimiter='|')
132+
# Name the columns and trim whitespace off the text fields.
133+
self.db.columns = ['formula', 'name', 'phase', 'filename']
134+
self.db["formula"] = self.db["formula"].map(str.strip)
135+
self.db["name"] = self.db["name"].map(str.strip)
136+
self.db["phase"] = self.db["phase"].map(str.strip)
137+
self.db["filename"] = self.db["filename"].map(str.strip)
138+
139+
# Make sure that the directory for cached JANAF files exists.
140+
self.JANAF_cachedir = os.path.join(dirname, 'JANAF_Cache')
141+
if not os.path.exists(self.JANAF_cachedir):
142+
os.mkdir(self.JANAF_cachedir)
143+
144+
def search(self, searchstr):
145+
"""
146+
List all the species containing a string. Helpful for
147+
interactive use of the database.
148+
returns a pandas dataframe containing valid phases.
149+
150+
>>> db = Janafdb()
151+
>>> s = db.search('Rb-')
152+
>>> print(s)
153+
formula name phase filename
154+
1710 Rb- Rubidium, Ion g Rb-007
155+
>>> s = db.search('Ti')
156+
>>> print(len(s))
157+
88
158+
"""
159+
160+
formulasearch = self.db['formula'].str.contains(searchstr)
161+
namesearch = self.db['name'].str.contains(searchstr)
162+
163+
return self.db[formulasearch | namesearch]
164+
165+
def getphasedata(self, formula=None, name=None, phase=None, nocache=False):
166+
"""
167+
Returns an element instance given the name of the element.
168+
formula, name and phase match the respective fields in the JANAF index.
169+
nocache = True means that we will always get the data from the web.
170+
171+
>>> db = Janafdb()
172+
>>> db.getphasedata(formula='O2Ti', phase='cr')
173+
Traceback (most recent call last):
174+
...
175+
ValueError: There are 2 records matching this pattern.
176+
>>> db.getphasedata(formula='Oxyz')
177+
Traceback (most recent call last):
178+
...
179+
ValueError: Valid phase types are ['cr', 'l', 'cr,l', 'g', 'ref', 'cd', 'fl', 'am', 'vit', 'mon', 'pol', 'sln', 'aq', 'sat'].
180+
>>> db.getphasedata(formula='Oxyz', phase='l')
181+
Traceback (most recent call last):
182+
...
183+
ValueError: Did not find Oxyz, None, (l)
184+
"""
185+
186+
# Check that the phase type requested is valid.
187+
if phase not in Janafdb.VALIDPHASETYPES:
188+
raise ValueError("Valid phase types are " + str(
189+
Janafdb.VALIDPHASETYPES) + ".")
190+
191+
# We can search on either an exact formula, partial text match in the
192+
# name, and exact phase type.
193+
formulasearch = pd.Series(np.ones(len(self.db)), dtype=bool)
194+
namesearch = formulasearch.copy()
195+
phasesearch = formulasearch.copy()
196+
if formula is not None:
197+
formulasearch = self.db['formula'] == formula
198+
if name is not None:
199+
namesearch = self.db['name'].str.contains(name)
200+
if phase is not None:
201+
phasesearch = self.db['phase'] == phase
202+
searchmatch = formulasearch & namesearch & phasesearch
203+
204+
# Get the record (should be one record) which specifies this phase.
205+
PhaseRecord = self.db[searchmatch]
206+
if len(PhaseRecord) == 0:
207+
raise ValueError("Did not find %s, %s, (%s)" %
208+
(formula, name, phase))
209+
if len(PhaseRecord) > 1:
210+
raise ValueError("There are %d records matching this pattern." %
211+
len(PhaseRecord))
212+
213+
# At this point we have one record. Check if we have that file cached.
214+
cachedfilename = os.path.join(
215+
self.JANAF_cachedir, PhaseRecord['filename'].values[0] + '.txt')
216+
if os.path.exists(cachedfilename) and not nocache:
217+
# Yes it was cached, so let's read it into memory.
218+
with open(cachedfilename, 'r') as f:
219+
textdata = f.read()
220+
else:
221+
# No it was not cached so let's get it from the web.
222+
response = urllib2.urlopen(Janafdb.JANAF_URL %
223+
PhaseRecord['filename'].values[0])
224+
textdata = response.read()
225+
226+
# And cache the data so we aren't making unnecessary trips to the
227+
# web.
228+
if not nocache:
229+
with open(cachedfilename, 'w') as f:
230+
f.write(textdata)
231+
232+
# Create a phase class and return it.
233+
return JanafPhase(textdata)
234+
235+
# def getmixturedata(self, components):
236+
# """
237+
# Creates a mixture of components given a list of tuples
238+
# containing the formula and the volume percent
239+
# """
240+
# mixture = Mixture()
241+
# for comp in components:
242+
# mixture.add(self.getelementdata(comp[0]), comp[1])
243+
#
244+
# return mixture

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ envlist = py27,py34
55
deps =
66
nose
77
numpy
8+
pandas
89
coverage
910
setuptools
1011
commands =

0 commit comments

Comments
 (0)