"""Contains the adsorbate class."""
from pygaps import logger
from pygaps.data import ADSORBATE_LIST
from pygaps.units.converter_unit import _PRESSURE_UNITS
from pygaps.units.converter_unit import c_unit
from pygaps.utilities.coolprop_utilities import CP
from pygaps.utilities.coolprop_utilities import thermodynamic_backend
from pygaps.utilities.exceptions import CalculationError
from pygaps.utilities.exceptions import ParameterError
# TODO: units in the prop dictionary and from coolprop do not always match (e.g. p_critical)
[docs]class Adsorbate():
"""
An unified class descriptor for an adsorbate.
Its purpose is to expose properties such as adsorbate name,
and formula, as well as physical properties, such as molar mass
vapour pressure, etc.
The properties can be either calculated through a wrapper over
CoolProp or supplied in the initial adsorbate creation.
All parameters passed are saved in a self.parameters
dictionary.
Parameters
----------
name : str
The name which should be used for this adsorbate.
Other Parameters
----------------
alias : list[str]
Other names the same adsorbate might take.
Example: name=propanol, alias=['1-propanol'].
pyGAPS disregards capitalisation (Propanol = propanol = PROPANOL).
formula : str
A chemical formula for the adsorbate in LaTeX form: He/N_{2}/C_{2}H_{4} etc.
backend_name : str
Used for integration with CoolProp/REFPROP. For a list of names
look at the CoolProp `list of fluids
<http://www.coolprop.org/fluid_properties/PurePseudoPure.html#list-of-fluids>`_
molar_mass : float
Custom value for molar mass (otherwise obtained through CoolProp).
p_triple : float
Custom value for triple point pressure (otherwise obtained through CoolProp).
t_triple : float
Custom value for triple point temperature (otherwise obtained through CoolProp).
p_critical : float
Custom value for critical point pressure (otherwise obtained through CoolProp).
t_critical : float
Custom value for critical point temperature (otherwise obtained through CoolProp).
saturation_pressure : float
Custom value for saturation pressure (otherwise obtained through CoolProp).
surface_tension : float
Custom value for surface tension (otherwise obtained through CoolProp).
liquid_density : float
Custom value for liquid density (otherwise obtained through CoolProp).
liquid_molar_density : float
Custom value for liquid molar density (otherwise obtained through CoolProp).
gas_density : float
Custom value for gas density (otherwise obtained through CoolProp).
gas_molar_density : float
Custom value for gas molar density (otherwise obtained through CoolProp).
enthalpy_vaporisation : float
Custom value for enthalpy of vaporisation/liquefaction (otherwise obtained through CoolProp).
enthalpy_liquefaction : float
Custom value for enthalpy of vaporisation/liquefaction (otherwise obtained through CoolProp).
Notes
-----
The members of the properties dictionary are left at the discretion of the
user, to keep the class extensible. There are, however, some unique
properties which are used by calculations in other modules listed in the
other parameters section above.
These properties can be either calculated by CoolProp (if the adsorbate
exists in CoolProp/REFPROP) or taken from the parameters dictionary. They
are best accessed using the associated function.
Calculated::
my_adsorbate.surface_tension(77)
Value from dictionary::
my_adsorbate.surface_tension(77, calculate=False)
If available, the underlying CoolProp state object
(http://www.coolprop.org/coolprop/LowLevelAPI.html) can be accessed directly
through the backend variable. For example, to get the CoolProp-calculated
critical pressure::
adsorbate.backend.p_critical()
"""
# special reserved parameters
_reserved_params = [
"name",
"alias",
"_state",
"_backend_mode",
]
def __init__(
self,
name: str,
store: bool = False,
**properties,
):
"""Instantiate by passing a dictionary with the parameters."""
# Adsorbate name
if name is None:
raise ParameterError("Must provide a name for the created adsorbate.")
self.name = name
# List of aliases
alias = properties.pop('alias', None)
# Generate list of aliases
_name = name.lower()
if alias is None:
self.alias = [_name]
else:
if isinstance(alias, str):
self.alias = [alias.lower()]
else:
self.alias = [a.lower() for a in alias]
if _name not in self.alias:
self.alias.append(_name)
#: Adsorbate properties
self.properties = properties
# CoolProp interaction variables, only generate when called
self._state = None
self._backend_mode = None
# Store reference in internal list
if store:
if self not in ADSORBATE_LIST:
ADSORBATE_LIST.append(self)
def __repr__(self):
"""Print adsorbate id."""
return f"<pygaps.Adsorbate '{self.name}'>"
def __str__(self):
"""Print adsorbate standard name."""
return self.name
def __hash__(self):
"""Override hashing as a name hash."""
return hash(self.name)
def __eq__(self, other):
"""Overload equality operator to include aliases."""
if isinstance(other, Adsorbate):
return self.name == other.name
return other.lower() in self.alias
def __add__(self, other):
"""Overload addition operator to use name."""
return self.name + other
def __radd__(self, other):
"""Overload rev addition operator to use name."""
return other + self.name
[docs] def print_info(self):
"""Print a short summary of all the adsorbate parameters."""
string = f"pyGAPS Adsorbate: '{self.name}'\n"
string += f"Aliases: { *self.alias,}\n"
if self.properties:
string += "Other properties: \n"
for prop, val in self.properties.items():
string += (f"\t{prop}: {str(val)}\n")
print(string)
[docs] @classmethod
def find(cls, name: str):
"""Get the specified adsorbate from the master list.
Parameters
----------
name : str
The name of the adsorbate to search
Returns
-------
Adsorbate
Instance of class
Raises
------
``ParameterError``
If it does not exist in list.
"""
# Skip search if already adsorbate
if isinstance(name, Adsorbate):
return name
if not isinstance(name, str):
raise ParameterError("Pass a string as an adsorbate name.")
# See if adsorbate exists in master list
try:
return next(ads for ads in ADSORBATE_LIST if ads == name)
except StopIteration:
raise ParameterError(
f"Adsorbate '{name}' does not exist in list of adsorbates. "
"First populate pygaps.ADSORBATE_LIST with required adsorbate class."
) from None
@property
def backend(self):
"""Return the CoolProp state associated with the fluid."""
if (not self._backend_mode or self._backend_mode != thermodynamic_backend()):
self._backend_mode = thermodynamic_backend()
self._state = CP.AbstractState(self._backend_mode, self.backend_name)
return self._state
@property
def formula(self) -> str:
"""Return the adsorbate formula."""
formula = self.properties.get('formula')
if formula is None:
return self.name
return formula
[docs] def to_dict(self) -> dict:
"""
Return a dictionary of the adsorbate class.
Is the same dictionary that was used to create it.
Returns
-------
dict
dictionary of all parameters
"""
parameters_dict = {
'name': self.name,
'alias': self.alias,
}
parameters_dict.update(self.properties)
return parameters_dict
[docs] def get_prop(self, prop: str):
"""
Return a property from the 'properties' dictionary.
Parameters
----------
prop : str
property name desired
Returns
-------
str/float
Value of property in the properties dict
Raises
------
``ParameterError``
If the the property does not exist
in the class dictionary.
"""
req_prop = self.properties.get(prop)
if req_prop is None:
raise ParameterError(
f"Adsorbate '{self.name}' does not have a property named "
f"'{prop}' in its 'parameters' dictionary. Consider adding it "
"manually if you need it and know its value."
)
return req_prop
@property
def backend_name(self) -> str:
"""
Get the CoolProp interaction name of the adsorbate.
Returns
-------
str
Value of backend_name in the properties dict
Raises
------
``ParameterError``
If the the property does not exist
in the class dictionary.
"""
c_name = self.properties.get("backend_name")
if c_name is None:
raise ParameterError(
f"Adsorbate '{self.name}' does not have a property named "
"backend_name. This must be available for CoolProp interaction."
)
return c_name
[docs] def molar_mass(self, calculate: bool = True) -> float:
"""
Return the molar mass of the adsorbate.
Parameters
----------
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Molar mass in g/mol.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
return self.backend.molar_mass() * 1000
except BaseException as err:
_warn_reading_params(err)
return self.molar_mass(calculate=False)
try:
return self.get_prop("molar_mass")
except ParameterError as err:
_raise_calculation_error(err)
[docs] def p_triple(self, calculate: bool = True) -> float:
"""
Return the triple point pressure, in Pa.
Parameters
----------
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Triple point pressure in Pa.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
# For some reason coolprop does not implement a python
# wrapper for P_triple, so we are directly calling the propsSI function
# TODO: this will not work for REFPROP
return CP.CoolProp.PropsSI('PTRIPLE', self.backend_name)
except BaseException as err:
_warn_reading_params(err)
return self.p_triple(calculate=False)
try:
return self.get_prop("p_triple") * 1e5
except ParameterError as err:
_raise_calculation_error(err)
[docs] def t_triple(self, calculate: bool = True) -> float:
"""
Return the triple point temperature, in K.
Parameters
----------
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Triple point temperature in K.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
return self.backend.Ttriple()
except BaseException as err:
_warn_reading_params(err)
return self.t_triple(calculate=False)
try:
return self.get_prop("t_triple")
except ParameterError as err:
_raise_calculation_error(err)
[docs] def p_critical(self, calculate: bool = True) -> float:
"""
Return the critical point pressure, in Pa.
Parameters
----------
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Critical point pressure in Pa.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
return self.backend.p_critical()
except BaseException as err:
_warn_reading_params(err)
return self.p_critical(calculate=False)
try:
return self.get_prop("p_critical") * 1e5
except ParameterError as err:
_raise_calculation_error(err)
[docs] def t_critical(self, calculate: bool = True) -> float:
"""
Return the critical point temperature, in K.
Parameters
----------
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Critical point temperature in K.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
return self.backend.T_critical()
except BaseException as err:
_warn_reading_params(err)
return self.t_critical(calculate=False)
try:
return self.get_prop("t_critical")
except ParameterError as err:
_raise_calculation_error(err)
[docs] def pressure_saturation(
self,
temp: float,
unit: str = None,
calculate: bool = True,
) -> float:
"""
Get the saturation pressure at a particular temperature, in desired unit (default Pa).
Alias for 'saturation_pressure'
Parameters
----------
temp : float
Temperature at which the pressure is desired in K.
unit : str
Unit in which to return the saturation pressure.
If not specifies defaults to Pascal.
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Pressure in unit requested.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
return self.saturation_pressure(temp, unit, calculate)
[docs] def saturation_pressure(
self,
temp: float,
unit: str = None,
calculate: bool = True,
) -> float:
"""
Get the saturation pressure at a particular temperature, in desired unit (default Pa).
Parameters
----------
temp : float
Temperature at which the pressure is desired in K.
unit : str
Unit in which to return the saturation pressure.
If not specifies defaults to Pascal.
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Pressure in unit requested.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
state = self.backend
state.update(CP.QT_INPUTS, 0.0, temp)
sat_p = state.p()
except BaseException as err:
_warn_reading_params(err)
sat_p = self.saturation_pressure(temp, unit=unit, calculate=False)
if unit is not None:
sat_p = c_unit(_PRESSURE_UNITS, sat_p, 'Pa', unit)
return sat_p
try:
return self.get_prop("saturation_pressure")
except ParameterError as err:
_raise_calculation_error(err)
[docs] def surface_tension(
self,
temp: float,
calculate: bool = True,
) -> float:
"""
Get the surface tension at a particular temperature, in mN/m.
Parameters
----------
temp : float
Temperature at which the surface_tension is desired in K.
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Surface tension in mN/m.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
state = self.backend
state.update(CP.QT_INPUTS, 0.0, temp)
return state.surface_tension() * 1000
except BaseException as err:
_warn_reading_params(err)
return self.surface_tension(temp, calculate=False)
try:
return self.get_prop("surface_tension")
except ParameterError as err:
_raise_calculation_error(err)
[docs] def liquid_density(
self,
temp: float,
calculate: bool = True,
) -> float:
"""
Get the liquid density at a particular temperature, in g/cm3.
Parameters
----------
temp : float
Temperature at which the liquid density is desired in K.
calculate : bool, optional.
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Liquid density in g/cm3.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
state = self.backend
state.update(CP.QT_INPUTS, 0.0, temp)
return state.rhomass() / 1000
except BaseException as err:
_warn_reading_params(err)
return self.liquid_density(temp, calculate=False)
try:
return self.get_prop("liquid_density")
except ParameterError as err:
_raise_calculation_error(err)
[docs] def liquid_molar_density(
self,
temp: float,
calculate: bool = True,
) -> float:
"""
Get the liquid molar density at a particular temperature, in mol/cm3.
Parameters
----------
temp : float
Temperature at which the liquid density is desired in K.
calculate : bool, optional.
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Molar liquid density in mol/cm3.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
state = self.backend
state.update(CP.QT_INPUTS, 0.0, temp)
return state.rhomolar() / 1e6
except BaseException as err:
_warn_reading_params(err)
return self.liquid_molar_density(temp, calculate=False)
try:
return self.get_prop("liquid_molar_density")
except ParameterError as err:
_raise_calculation_error(err)
[docs] def gas_density(
self,
temp: float,
calculate: bool = True,
) -> float:
"""
Get the gas molar density at a particular temperature, in g/cm3.
Parameters
----------
temp : float
Temperature at which the gas density is desired in K.
calculate : bool, optional.
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Gas density in g/cm3.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
state = self.backend
state.update(CP.QT_INPUTS, 1.0, temp)
return state.rhomass() / 1000
except BaseException as err:
_warn_reading_params(err)
return self.gas_density(temp, calculate=False)
try:
return self.get_prop("gas_density")
except ParameterError as err:
_raise_calculation_error(err)
[docs] def gas_molar_density(
self,
temp: float,
calculate: bool = True,
) -> float:
"""
Get the gas density at a particular temperature, in mol/cm3.
Parameters
----------
temp : float
Temperature at which the gas density is desired in K.
calculate : bool, optional.
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Molar gas density in mol/cm3.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
try:
state = self.backend
state.update(CP.QT_INPUTS, 1.0, temp)
return state.rhomolar() / 1e6
except BaseException as err:
_warn_reading_params(err)
return self.gas_molar_density(temp, calculate=False)
try:
return self.get_prop("gas_molar_density")
except ParameterError as err:
_raise_calculation_error(err)
[docs] def enthalpy_vaporisation(
self,
temp: float = None,
press: float = None,
calculate: bool = True,
) -> float:
"""
Get the enthalpy of vaporisation at a particular temperature, in kJ/mol.
Parameters
----------
temp : float
Temperature at which the enthalpy of vaporisation is desired, in K.
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Enthalpy of vaporisation in kJ/mol.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
return self.enthalpy_liquefaction(temp, press, calculate)
[docs] def enthalpy_liquefaction(
self,
temp: float = None,
press: float = None,
calculate: bool = True,
) -> float:
"""
Get the enthalpy of liquefaction at a particular temperature, in kJ/mol.
Parameters
----------
temp : float
Temperature at which the enthalpy of liquefaction is desired, in K.
calculate : bool, optional
Whether to calculate the property or look it up in the properties
dictionary, default - True.
Returns
-------
float
Enthalpy of liquefaction in kJ/mol.
Raises
------
``ParameterError``
If the calculation is not requested and the property does not exist
in the class dictionary.
``CalculationError``
If it cannot be calculated, due to a physical reason.
"""
if calculate:
if temp and press:
raise CalculationError(
"Can only specify one intensive variable, either temperature or pressure."
)
try:
state = self.backend
if temp:
state.update(CP.QT_INPUTS, 0.0, temp)
h_liq = state.hmolar()
state.update(CP.QT_INPUTS, 1.0, temp)
h_vap = state.hmolar()
elif press:
state.update(CP.PQ_INPUTS, press, 0.0)
h_liq = state.hmolar()
state.update(CP.PQ_INPUTS, press, 1.0)
h_vap = state.hmolar()
else:
raise CalculationError("Neither pressure nor temperature specified.")
return (h_vap - h_liq) / 1000
except BaseException as err:
_warn_reading_params(err)
return self.enthalpy_liquefaction(temp, calculate=False)
try:
return self.get_prop("enthalpy_liquefaction")
except ParameterError as err:
_raise_calculation_error(err)
def _warn_reading_params(err):
logger.warning(
f"Thermodynamic backend failed with error: {err}. "
"Attempting to read parameters dictionary..."
)
def _raise_calculation_error(err):
raise CalculationError(
f"Thermodynamic backend failed (see traceback for error): {err}"
) from err