This notebook is part of the pyglenn Labbook collection. ⬇ Download .ipynb

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,

\[C_p^\circ(T), \qquad H^\circ(T), \qquad S^\circ(T),\]

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:

  1. Connecting to the bundled database

  2. Inspecting the database contents

  3. Searching for species

  4. Computing \(C_p^\circ\), \(H^\circ\) and \(S^\circ\) at a given temperature

  5. Understanding the values that are returned

  6. Enthalpy differences and property tables

  7. Graceful error handling

Author of the package: Dr. Reginaldo G. Leão Jr. — GESESC / IFMG.

Contents

  1. Connecting to the database

  2. Inspecting the database

  3. Searching for species

  4. Computing properties at a temperature

  5. Enthalpy differences (sensible heat)

  6. Property tables over many temperatures

  7. 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

temperature

The temperature you asked for

K

cp

Standard-state heat capacity \(C_p^\circ(T)\)

J/(mol·K)

s

Standard-state absolute (Third-Law) entropy \(S^\circ(T)\)

J/(mol·K)

h_relative

Standardized molar enthalpy \(H^\circ(T)\) on the NASA scale

J/mol

temp_interval

[T_min, T_max] of the polynomial piece that was used

K

species_name

Resolved species name

phase

gas or condensed

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_relative as the standardized enthalpy (which contains \(\Delta_f H^\circ\));

  • build tables with get_properties_range() and enthalpy differences with calculate_enthalpy_change().

Next: notebook 02 opens up the NASA polynomial machinery itself and shows how these numbers are computed from the raw coefficients.