01 - Getting Started with pyglenn
``pyglenn`` is a lightweight, dependency-free thermochemical properties calculator. It reconstructs three standard-state molar properties as analytical functions of temperature,
from NASA polynomial coefficients stored in a bundled SQLite database. The database ships with the package and contains roughly 2030 chemical species (gases and condensed phases) spanning 3772 temperature intervals, derived from NASA Glenn’s thermo.inp data set.
This first notebook covers the essentials:
Connecting to the bundled database
Inspecting the database contents
Searching for species
Computing \(C_p^\circ\), \(H^\circ\) and \(S^\circ\) at a given temperature
Understanding the values that are returned
Enthalpy differences and property tables
Graceful error handling
Author of the package: Dr. Reginaldo G. Leão Jr. — GESESC / IFMG.
Contents
Connecting to the database
Inspecting the database
Searching for species
Computing properties at a temperature
Enthalpy differences (sensible heat)
Property tables over many temperatures
Error handling
Installation
pyglenn requires only Python ≥ 3.9 (SQLite3 is in the standard library). Install it from PyPI, conda-forge or from source:
pip install pyglenn
# or via conda:
conda install conda-forge::pyglenn numpy pandas matplotlib scipy
# or, from a local checkout:
pip install .
The commands below assume the package is already importable.
[1]:
from pyglenn import ThermochemicalCalculator, R
print("Universal gas constant R =", R, "J/(mol.K)")
Universal gas constant R = 8.314462618 J/(mol.K)
1. Connecting to the database
ThermochemicalCalculator is the high-level entry point. Because it manages a SQLite connection, the recommended pattern is the context manager, which opens the connection on entry and closes it on exit — even if an exception is raised:
with ThermochemicalCalculator() as calc:
... # calc is connected here
# connection automatically closed here
No path is needed: the constructor defaults to the thermo.db file bundled inside the package.
[2]:
with ThermochemicalCalculator() as calc:
print("Connected? ", calc.connected)
print("Connected after the block?", calc.connected)
Connected? True
Connected after the block? False
Manual connection management
If you prefer explicit control (for example inside a long-lived service), you can call connect() and close() yourself. connect() returns True on success.
[3]:
calc = ThermochemicalCalculator()
ok = calc.connect()
print("connect() ->", ok)
print("calc.connected ->", calc.connected)
calc.close()
print("after close ->", calc.connected)
connect() -> True
calc.connected -> True
after close -> False
2. Inspecting the database
The underlying query object is exposed as calc.db (a ThermoDBQuery instance). Its get_statistics() method gives a quick overview of the data set.
[4]:
with ThermochemicalCalculator() as calc:
stats = calc.db.get_statistics()
for key, value in stats.items():
print(f"{key:22s}: {value}")
total_species : 2030
total_intervals : 3772
total_coeff_sets : 3772
species_by_phase : {'condensed': 766, 'gas': 1264}
avg_molecular_weight : 3143009517.295669
Note that species_by_phase distinguishes gas species from condensed (liquid/solid) species. The database mixes both, so when you look up a fuel or a product you may find several entries — a gas phase, a liquid phase, and sometimes multiple crystalline forms.
3. Searching for species
get_available_species(pattern) returns a list of dictionaries. With a non-empty pattern it does a partial match on both the name and the chemical formula (limited to 20 hits); with an empty pattern it paginates through the entire catalogue.
[5]:
with ThermochemicalCalculator() as calc:
matches = calc.get_available_species("H2O")
print(f"{len(matches)} matches for 'H2O':\n")
for s in matches:
print(f" id={s['id']:5d} {s['name']:18s} {s['phase']:9s} M={s['molecular_weight']:.4f} g/mol")
8 matches for 'H2O':
id= 672 H2O gas M=18.0153 g/mol
id= 293 CH2OH gas M=31.0339 g/mol
id= 294 CH2OH+ gas M=31.0334 g/mol
id= 1510 H2O(L) condensed M=18.0153 g/mol
id= 1509 H2O(cr) condensed M=18.0153 g/mol
id= 673 H2O+ gas M=18.0147 g/mol
id= 674 H2O2 gas M=34.0147 g/mol
id= 847 NH2OH gas M=33.0299 g/mol
Because the search is a substring match, the query above returns CH2OH, H2O2, NH2OH, … alongside water itself. When you need a specific species, pass exact_match=True for a case-insensitive exact lookup:
[6]:
with ThermochemicalCalculator() as calc:
for name in ["O2", "N2", "CO2", "H2O", "CH4", "Ar"]:
sid = calc.get_available_species(name, exact_match=True)[0]["id"]
print(f"{name:5s} -> database id {sid}")
O2 -> database id 931
N2 -> database id 861
CO2 -> database id 316
H2O -> database id 672
CH4 -> database id 296
Ar -> database id 74
Browsing the whole catalogue
Passing an empty string paginates through all ~2030 species. Here we just count them and preview the first handful.
[7]:
with ThermochemicalCalculator() as calc:
everything = calc.get_available_species("")
print(f"Total species in catalogue: {len(everything)}\n")
print("First 8 (alphabetical):")
for s in everything[:8]:
print(f" {s['name']:20s} {s['phase']}")
Total species in catalogue: 2030
First 8 (alphabetical):
(CH3COOH)2 gas
(HCOOH)2 gas
(WO3)2 gas
(WO3)3 gas
(WO3)4 gas
(WO3)5 gas
-1.302004763D+06 condensed
-7.310769440D+05 condensed
4. Computing properties at a temperature
calculate_properties(species_id, temperature_K) is the core method. It returns a dictionary of standard-state properties evaluated at the requested temperature. Let’s evaluate molecular oxygen at 298.15 K and at 1000 K.
[8]:
with ThermochemicalCalculator() as calc:
o2 = calc.get_available_species("O2", exact_match=True)[0]["id"]
for T in (298.15, 1000.0):
props = calc.calculate_properties(o2, T)
print(f"--- O2 at {T} K ---")
for k, v in props.items():
print(f" {k:14s}: {v}")
print()
--- O2 at 298.15 K ---
temperature : 298.15
cp : 29.37818595837342
h_relative : -1.2807210325893945e-05
s : 205.14829827953108
temp_interval : [200.0, 1000.0]
species_name : O2
phase : gas
--- O2 at 1000.0 K ---
temperature : 1000.0
cp : 34.882346223554805
h_relative : 22707.08129523519
s : 243.58592574388805
temp_interval : [200.0, 1000.0]
species_name : O2
phase : gas
What do these numbers mean?
Key |
Meaning |
Units |
|---|---|---|
|
The temperature you asked for |
K |
|
Standard-state heat capacity \(C_p^\circ(T)\) |
J/(mol·K) |
|
Standard-state absolute (Third-Law) entropy \(S^\circ(T)\) |
J/(mol·K) |
|
Standardized molar enthalpy \(H^\circ(T)\) on the NASA scale |
J/mol |
|
|
K |
|
Resolved species name |
— |
|
|
— |
A crucial point about ``h_relative``: NASA polynomials fit the standardized enthalpy, which already includes the enthalpy of formation. As a consequence:
for a species in its elemental reference state (e.g. O₂, N₂, graphite), \(H^\circ(298.15\,\text{K}) \approx 0\);
for a compound, \(H^\circ(298.15\,\text{K})\) equals its enthalpy of formation \(\Delta_f H^\circ\).
This is why oxygen’s h_relative above is essentially zero at 298.15 K. We exploit this property in later notebooks to compute reaction enthalpies directly. Quick sanity check for water vapour:
[9]:
with ThermochemicalCalculator() as calc:
h2o = calc.get_available_species("H2O", exact_match=True)[0]["id"]
hf = calc.calculate_properties(h2o, 298.15)["h_relative"]
print(f"H2O(g) standardized enthalpy at 298.15 K = {hf/1000:8.2f} kJ/mol")
print("Literature enthalpy of formation of H2O(g) = -241.83 kJ/mol")
H2O(g) standardized enthalpy at 298.15 K = -241.82 kJ/mol
Literature enthalpy of formation of H2O(g) = -241.83 kJ/mol
5. Enthalpy differences (sensible heat)
The change in enthalpy needed to heat one mole of a substance from \(T_1\) to \(T_2\) (its sensible enthalpy) is \(H^\circ(T_2) - H^\circ(T_1)\). calculate_enthalpy_change computes this directly.
[10]:
with ThermochemicalCalculator() as calc:
n2 = calc.get_available_species("N2", exact_match=True)[0]["id"]
dH = calc.calculate_enthalpy_change(n2, 300.0, 1200.0)
print(f"Heating 1 mol N2 from 300 K to 1200 K requires {dH/1000:.2f} kJ")
Heating 1 mol N2 from 300 K to 1200 K requires 28.05 kJ
6. Property tables over many temperatures
get_properties_range(species_id, [T1, T2, ...]) evaluates the properties at a list of temperatures and returns a dict keyed by temperature. Temperatures that fall outside every valid interval are silently skipped.
[11]:
temps = [300, 500, 800, 1200, 1800, 2500]
with ThermochemicalCalculator() as calc:
co2 = calc.get_available_species("CO2", exact_match=True)[0]["id"]
table = calc.get_properties_range(co2, temps)
print(f"{'T [K]':>7} {'Cp [J/mol/K]':>14} {'S [J/mol/K]':>13} {'H [kJ/mol]':>12}")
for T in temps:
p = table[T]
print(f"{T:7.0f} {p['cp']:14.3f} {p['s']:13.3f} {p['h_relative']/1000:12.3f}")
T [K] Cp [J/mol/K] S [J/mol/K] H [kJ/mol]
300 37.220 214.016 -393.439
500 44.624 234.896 -385.201
800 51.432 257.491 -370.699
1200 56.347 279.388 -349.030
1800 59.693 302.964 -314.076
2500 61.443 322.881 -271.603
Notice how \(C_p\) and \(S\) of CO₂ grow strongly with temperature — a hallmark of polyatomic molecules whose vibrational modes become increasingly excited. Notebook 03 explores this behaviour graphically.
7. Error handling
pyglenn raises specific exceptions instead of returning None:
``TemperatureOutOfRangeError`` — the requested temperature is outside the species’ valid range;
``DatabaseNotConnectedError`` — a method is called before connecting;
``SpeciesNotFoundError`` — an invalid species ID is supplied.
All inherit from ``ThermoCalcError``, so you can catch them individually or together with try/except.
[ ]:
from pyglenn import ThermoCalcError, TemperatureOutOfRangeError, DatabaseNotConnectedError
print("--- Temperature out of range ---")
with ThermochemicalCalculator() as calc:
o2 = calc.get_available_species("O2", exact_match=True)[0]["id"]
# O2 gas is valid from 200 K to 6000 K, but 50 000 K is out of range:
try:
props = calc.calculate_properties(o2, 50_000.0)
except TemperatureOutOfRangeError as e:
print(f"Caught: {e}")
print("\n--- Calling before connecting ---")
lonely = ThermochemicalCalculator()
try:
props = lonely.calculate_properties(o2, 300.0)
except DatabaseNotConnectedError as e:
print(f"Caught: {e}")
---------------------------------------------------------------------------
TemperatureOutOfRangeError Traceback (most recent call last)
Cell In[12], line 4
1 with ThermochemicalCalculator() as calc:
2 o2 = calc.get_available_species("O2", exact_match=True)[0]["id"]
3 # O2 gas is valid up to 20000 K, but 50000 K is out of range:
----> 4 out_of_range = calc.calculate_properties(o2, 50_000.0)
5 print("calculate_properties at 50000 K ->", out_of_range)
6
7 # Calling before connecting returns None instead of crashing:
File ~\miniconda3\envs\ct-env\Lib\site-packages\pyglenn\calculator.py:192, in ThermochemicalCalculator.calculate_properties(self, species_id, temperature)
188 if not interval_data:
189 intervals = [
190 (i['temp_min'], i['temp_max']) for i in species_data['intervals']
191 ]
--> 192 raise TemperatureOutOfRangeError(
193 f'Temperature {temperature:.1f} K out of valid range '
194 f"for species '{species_data['name']}'. "
195 f'Available intervals: {intervals}'
196 )
198 coeffs: dict[str, float] = interval_data['coefficients']
200 # Dimensionless properties (÷ R)
TemperatureOutOfRangeError: Temperature 50000.0 K out of valid range for species 'O2'. Available intervals: [(200.0, 1000.0), (1000.0, 6000.0), (6000.0, 20000.0)]
[ ]:
from pyglenn import (
ThermoCalcError,
DatabaseNotConnectedError,
SpeciesNotFoundError,
TemperatureOutOfRangeError,
)
# All inherit from ThermoCalcError, so you can catch the whole family at once.
for exc in (DatabaseNotConnectedError, SpeciesNotFoundError, TemperatureOutOfRangeError):
print(f"{exc.__name__:28s} is a ThermoCalcError? {issubclass(exc, ThermoCalcError)}")
Summary
You now know how to:
connect to the bundled database (preferably via the context manager);
inspect it with
get_statistics();search with
get_available_species()and resolve exact names;compute \(C_p^\circ\), \(S^\circ\) and \(H^\circ\) with
calculate_properties();interpret
h_relativeas the standardized enthalpy (which contains \(\Delta_f H^\circ\));build tables with
get_properties_range()and enthalpy differences withcalculate_enthalpy_change().
Next: notebook 02 opens up the NASA polynomial machinery itself and shows how these numbers are computed from the raw coefficients.