Thermodynamic Interfaces
Cantera
rgfrosh
has native support for cantera.ThermoPhase
instances. As of the Cantera 3.0 release,
real gas equations of state are also supported.
CoolProp
The CPInterface
wrapper is
provided to enable straightforward use of CoolProp.AbstractState
instances with rgfrosh
:
from rgfrosh import FrozenShock
from rgfrosh.thermo import CPInterface
import CoolProp as CP
state = CP.AbstractState("PR", "Argon") # Peng-Robinson
state.specify_phase(CP.iphase_supercritical_gas) # (1)!
shock = FrozenShock(CPInterface(state), u1=763, T1=293, P1=(10 * 101325))
- Specifying phase is necessary if the cubic has three roots
User-defined Interfaces
If you would like to use rgfrosh
with thermodynamic routines other than Cantera or
CoolProp, all you have to do is pass an object that matches the pattern defined by the
ThermoInterface
class.
Note
The ThermoInterface
class utilizes structural
subtyping (see PEP 544) through the
built-in typing.Protocol
base class. Thus, any class that implements the required methods is considered a
subtype, even if it is not an explicit subclass of ThermoInterface
; however,
explicitly subclassing ThermoInterface
is recommended for user-defined
interfaces to ensure all required methods are defined.
Below is a simple example implementation of an interface for a calorically perfect gas:
from rgfrosh import ThermoInterface
from rgfrosh.constants import GAS_CONSTANT
class PerfectGas(ThermoInterface):
"""Thermo interface for a calorically perfect gas."""
def __init__(self, gamma, MW):
self._T = 300
self._P = 101325
self.gamma = gamma
self.MW = MW
@property
def TP(self):
return self._T, self._P
@TP.setter
def TP(self, value):
self._T, self._P = value
@property
def mean_molecular_weight(self):
return self.MW
@property
def density_mass(self):
return self._P / (GAS_CONSTANT / self.MW * self._T)
@property
def cp_mass(self):
return self.gamma / (self.gamma - 1) * GAS_CONSTANT / self.MW
@property
def enthalpy_mass(self):
return self.cp_mass * self._T
@property
def isothermal_compressibility(self):
return 1 / self._P
@property
def thermal_expansion_coeff(self):
return 1 / self._T
The interface can then be used with FrozenShock
:
from rgfrosh import FrozenShock
argon = PerfectGas(5/3, 40)
frozen_shock = FrozenShock(argon, u1=750, P1=101325)
print(f"T5 = {frozen_shock.T5:.1f} K, P5 = {frozen_shock.P5 / 101325:.2f} atm")
which yields the reflected shock conditions:
T5 = 1353.9 K, P5 = 23.60 atm
For the case of a calorically perfect gas, the frozen shock solution should simplify to the
ideal shock solution. We can use the custom interface with the
IdealShock.from_thermo
classmethod to verify this:
from rgfrosh import IdealShock
ideal_shock = IdealShock.from_thermo(argon, u1=750, P1=101325)
print(f"T5 = {frozen_shock.T5:.1f} K, P5 = {frozen_shock.P5 / 101325:.2f} atm")
which yields approximately the same solution:
T5 = 1353.9 K, P5 = 23.60 atm