Solving thermodynamics problems
Contents
Solving thermodynamics problems#
This module introduces how to solve thermodynamics problems in Python using Cantera and Pint. It will also briefly show how CoolProp could also be used, if you need access to a wider range of fluids than Cantera currently supports.
For help running these examples or setting up your own problems, see the module on “Setting up your computing environment”.
First, we need to import the necessary libraries:
# Numpy adds some useful numerical types and functions
import numpy as np
# Cantera will handle thermodynamic properties
import cantera as ct
# Pint gives us some helpful unit conversion
from pint import UnitRegistry
ureg = UnitRegistry()
Q_ = ureg.Quantity # We will use this to construct quantities (value + unit)
Quick Pint intro#
Pint is a useful tool for parsing unit expressions, converting between different units, and working with expressions involving quantities (values + units).
For example, we can read in expressions that give temperature and pressure in US customary units (e.g., imperial units) and then convert those to SI units:
Pint interprets nearly all properties either written out (e.g., kelvin
, meters
) or as abbreviations (e.g., K
, m
)—except for temperatures. In that case, we need to use kelvin
or K
, celsius
or degC
, fahrenheits
or degF
, and rankine
or degR
.
temperature = Q_(100, 'degF')
pressure = Q_('10 psi')
new_temperature = temperature.to('K')
new_pressure = pressure.to('Pa')
print(f'Temperature in SI: {new_temperature: .2f}')
print(f'Pressure in SI: {new_pressure: .2f}')
Temperature in SI: 310.93 kelvin
Pressure in SI: 68947.57 pascal
Another way to specify units is to multiply the value by an object with the unit (as a member of the ureg
object):
distance = 10 * ureg.meter
print(distance)
10 meter
Pint also handles mathematical operations between physical quantities:
distance1 = Q_(1, 'm')
distance2 = Q_(10, 'cm')
print(distance1 + distance2)
1.1 meter
mass = Q_(1, 'kg')
velocity = Q_(2, 'm/s')
kinetic_energy = 0.5 * mass * velocity**2
print(kinetic_energy)
print(kinetic_energy.to('joule'))
2.0 kilogram * meter ** 2 / second ** 2
2.0 joule
Using Cantera for thermodynamic properties#
Cantera is a suite of tools for solving problems involving chemical kinetics, thermodynamics, and transport processes. We’ll use it here primarily for evaluating thermodynamic properties of fluids.
Cantera comes with built-in liquid/vapor equations of state for multiple fluids:
water:
ct.Water()
nitrogen:
ct.Nitrogen()
methane:
ct.Methane()
hydrogen (H2):
ct.Hydrogen()
oxygen (O2):
ct.Oxygen()
carbon dioxide (CO2):
ct.CarbonDioxide()
n-heptane (C7H16):
ct.Heptane()
R134a:
ct.Hfc134a()
Let’s take a look at water, and evaluate its properties at different conditions:
# Create an object to hold the thermodynamic state of water
f = ct.Water()
# Fix the thermodynamic state by specifying temperature and
# specific volume, in SI units (K and m^3/kg)
f.TV = 673.15, 1e-2
# Evaluating the object provides a summary of its properties at this state
f()
water:
temperature 673.15 K
pressure 1.9936e+07 Pa
density 100 kg/m^3
mean mol. weight 18.016 kg/kmol
vapor fraction 1
phase of matter supercritical
1 kg 1 kmol
--------------- ---------------
enthalpy -1.315e+07 -2.3692e+08 J
internal energy -1.335e+07 -2.4051e+08 J
entropy 9078.4 1.6356e+05 J/K
Gibbs function -1.9261e+07 -3.4701e+08 J
heat capacity c_p 6284.6 1.1322e+05 J/K
heat capacity c_v 2694 48535 J/K
We can do the same thing to get the properties of R134a; this time, let’s use Pint to interpret the temperature and pressure as given in units of °C and bar, then convert to SI as needed by Cantera:
f = ct.Hfc134a()
# Specify temperature and pressure
temp = Q_(50, 'degC')
pres = Q_(3, 'bar')
# Fix the thermodynamic state using temperature and pressure
# Remember, Cantera requires SI units
f.TP = temp.to('K').magnitude, pres.to('Pa').magnitude
# see overview of properties
f()
HFC-134a:
temperature 323.15 K
pressure 3e+05 Pa
density 11.944 kg/m^3
mean mol. weight 102.03 kg/kmol
vapor fraction 1
phase of matter gas
1 kg 1 kmol
--------------- ---------------
enthalpy 2.4319e+05 2.4813e+07 J
internal energy 2.1807e+05 2.225e+07 J
entropy 1607.4 1.64e+05 J/K
Gibbs function -2.7623e+05 -2.8184e+07 J
heat capacity c_p 917.05 93568 J/K
heat capacity c_v 813.22 82975 J/K
Once we have the thermodynamic state fixed for a fluid, we can easily obtain any properties we need:
# get internal energy in SI units (mass basis)
print(f'Internal energy: {f.u: .2f} J/kg')
# enthalpy
print(f'Enthalpy: {f.h: .2f} J/kg')
# constant pressure specific heat
print(f'c_p: {f.cp: .2f} J')
# constant volume specific heat
print(f'c_v: {f.cv: .2f} J')
# density
print(f'density: {f.density: .2f} kg/m^3')
# critical properties
print(f'critical temperature: {f.critical_temperature: .2f} K')
print(f'critical pressure: {f.critical_pressure: .2f} Pa')
print(f'critical density: {f.critical_density: .2f} kg/m^3')
Internal energy: 218070.47 J/kg
Enthalpy: 243187.37 J/kg
c_p: 917.05 J
c_v: 813.22 J
density: 11.94 kg/m^3
critical temperature: 374.21 K
critical pressure: 4059280.00 Pa
critical density: 511.95 kg/m^3
We can fix the thermodynamic state by any combination of two properties:
# specify pressure and internal enegy
pres = Q_(250e3, 'Pa')
internal_energy = Q_(300e3, 'J/kg')
f.UP = internal_energy.to('J/kg').magnitude, pres.to('Pa').magnitude
# get specific volume
print(f'Specific volume: {f.v: 0.4f} m^3/kg')
Specific volume: 0.1332 m^3/kg
Convenience function to get values in SI units#
It can be a pain (and requires more writing) to constantly convert to SI units and extract the magnitude, when using Pint quantities in combination with Cantera and CoolProp.
We can define a handy convenience function to make this easier:
# for convenience:
def to_si(quant):
'''Converts a Pint Quantity to magnitude at base SI units.
'''
return quant.to_base_units().magnitude
You’ll need to define this function in any Python code or Jupyter notebook when you want to use it, but then it makes things much easier. For example:
temperature = Q_(30, 'degC')
pressure = Q_(1, 'atm')
f = ct.Water()
f.TP = to_si(temperature), to_si(pressure)
f()
water:
temperature 303.15 K
pressure 1.0132e+05 Pa
density 995.73 kg/m^3
mean mol. weight 18.016 kg/kmol
vapor fraction 0
phase of matter liquid
1 kg 1 kmol
--------------- ---------------
enthalpy -1.5845e+07 -2.8546e+08 J
internal energy -1.5845e+07 -2.8546e+08 J
entropy 3956.8 71286 J/K
Gibbs function -1.7044e+07 -3.0707e+08 J
heat capacity c_p 4178.6 75282 J/K
heat capacity c_v 4116.1 74156 J/K
Compare ideal and real gas behavior for nitrogen#
Using Cantera’s built-in pure fluid model for nitrogen, and its ideal gas model for air (which includes nitrogen), we can see an example of the error involved with treating fluids as ideal gases:
temp1 = Q_(500, 'K')
temp2 = Q_(1270, 'K')
pres = Q_(500, 'kPa')
# Real gas
air_real1 = ct.Nitrogen()
air_real1.TP = temp1.to('K').magnitude, pres.to('Pa').magnitude
air_real2 = ct.Nitrogen()
air_real2.TP = temp2.to('K').magnitude, pres.to('Pa').magnitude
delta_u_real = Q_(air_real2.u - air_real1.u, 'J/kg')
print(f'Δu for real gas: {delta_u_real: .2f}')
# ideal gas
air_ideal1 = ct.Solution('air.cti')
air_ideal1.TPX = temp1.to('K').magnitude, pres.to('Pa').magnitude, 'N2:1.0'
air_ideal2 = ct.Solution('air.cti')
air_ideal2.TPX = temp2.to('K').magnitude, pres.to('Pa').magnitude, 'N2:1.0'
delta_u_ideal = Q_(air_ideal2.u - air_ideal1.u, 'J/kg')
print(f'Δu for ideal gas: {delta_u_ideal: .2f}')
diff = 100*np.abs(delta_u_ideal-delta_u_real)/delta_u_real
print(f'difference: {diff.magnitude: .2f}%')
Δu for real gas: 647294.70 joule / kilogram
Δu for ideal gas: 648460.39 joule / kilogram
difference: 0.18%
/tmp/ipykernel_4736/2667467038.py:16: DeprecationWarning: XML_Node::build:
The CTI and XML input file formats are deprecated and will be removed in
Cantera 3.0. Use 'cti2yaml.py' or 'ctml2yaml.py' to convert CTI or XML input
files to the YAML format. See https://cantera.org/tutorials/legacy2yaml.html
for more information.
air_ideal1 = ct.Solution('air.cti')
Fortunately, at these conditions, nitrogen behaves much like an ideal gas, and so our calculations of the internal energy difference between two states are quite close.
Modeling air#
Air is a pseudo-pure fluid; in reality it is a multicomponent gas mixture of mostly oxygen and nitrogen, with trace amounts of arfon, carbon dioxide, and water (depending on the humidity). However, since the composition does not change under most processes, we can sometimes treat it like a pure fluid.
Using Cantera#
To solve problems with air, we can use an ideal gas model in Cantera, or the pseudo-pure fluid model in CoolProp (see the CoolProp module for more details).
In Cantera, we create a Solution
object using the built-in air.cti
model file, then specify the state, including composition. We can use mole fractions (X
) or mass fractions (Y
). The commonly accepted composition of air is 1 mole of oxygen to 3.76 moles of nitrogen, and we can specify this molar composition using the string 'O2:1.0, N2:3.76'
:
air = ct.Solution('air.cti')
air.TPX = 300.0, 101325.0, 'O2:1.0, N2:3.76'
air()
print(f'Density: {air.density: .3f} kg/m^3')
air:
temperature 300 K
pressure 1.0133e+05 Pa
density 1.172 kg/m^3
mean mol. weight 28.851 kg/kmol
phase of matter gas
1 kg 1 kmol
--------------- ---------------
enthalpy 1907.6 55035 J
internal energy -84548 -2.4393e+06 J
entropy 6891.7 1.9883e+05 J/K
Gibbs function -2.0656e+06 -5.9594e+07 J
heat capacity c_p 1010.1 29141 J/K
heat capacity c_v 721.87 20827 J/K
mass frac. Y mole frac. X chem. pot. / RT
--------------- --------------- ---------------
O2 0.233 0.21008 -26.234
N2 0.767 0.78992 -23.269
[ +6 minor] 0 0
Density: 1.172 kg/m^3
A Solution
object can be used to specify and compute thermodynamic, chemical kinetic, and/or transport properties of a mixture; the thermodynamics come from the ThermoPhase
class.
We can set the state by using a combination of two intensive properties, from the list [DP, HP, SP, SV, TD, TP, UV]
, along with the mole or mass fractions. Here, D
is density, H
is enthalpy, P
is pressure, S
is entropy, V
is specific volume, and T
is temperature; all should be in SI units, either in a mass or molar basis.
For example, we could specify the temperature, pressure, and air mole fractions using
air.TPX = 300, 101325, 'O2:1.0, N2:3.76'
or we could specify via density and pressure:
air.DPX = 1.225, 101325, 'O2:1.0, N2:3.76'
(Remember that density and specific volume are inversely related, so if we have one we always have the other.)
Using CoolProp#
We can also use CoolProp to model air, using its psuedo-pure fluid model. In this case, we do not need to specify the composition.
from CoolProp.CoolProp import PropsSI
density = PropsSI('D', 'T', 300.0, 'P', 101325, 'air')
print(f'Density: {density: .3f} kg/m^3')
Density: 1.177 kg/m^3
Hmm, the density given by CoolProp differs slightly from that given by Cantera. The difference is less than 1%, but where could that be coming from?
Well, our specified moles for the composition are only approximate, and also missing a small amount of argon. Let’s use a slightly more accurate mixture (though this is still missing traces of carbon dioxide, neon, helium, etc., that are present at less than 0.01% by mole):
air = ct.Solution('air.cti')
air.TPX = 300.0, 101325.0, 'N2:0.78084, O2:0.20947, Ar:0.00934'
print(f'Density: {air.density: .3f} kg/m^3')
Density: 1.176 kg/m^3