Source code for thermosteam._chemical

# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
# 
# This module is under the UIUC open-source license. See 
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
"""
import thermosteam as tmo
from warnings import warn
from flexsolve import IQ_interpolation, aitken_secant
from chemicals.identifiers import pubchem_db
from chemicals.phase_change import (Tb as normal_boiling_point_temperature,
                                    Tm as normal_melting_point_temperature,
                                    Hfus as heat_of_fusion)
from chemicals.critical import (Ihmels,
                                Tc as critical_point_temperature,
                                Pc as critical_point_pressure,
                                Vc as critical_point_volume)
from chemicals.acentric import (omega as acentric_factor,
                                omega_definition as acentric_factor_definition,
                                LK_omega as acentric_factor_LK,
                                Stiel_polar_factor as compute_Stiel_Polar)
from chemicals.triple import (Tt as triple_point_temperature,
                              Pt as triple_point_pressure)
from chemicals.combustion import combustion_data, combustion_stoichiometry
from chemicals.reaction import (
    Hf as heat_of_formation,
    S0 as absolute_entropy_of_formation
)
from chemicals.elements import (
    similarity_variable as compute_similarity_variable, 
    molecular_weight as compute_molecular_weight,
    charge_from_formula,
    atoms_to_Hill,
    get_atoms,
)
from chemicals.dipole import dipole_moment
from .free_energy import (
    Enthalpy, Entropy, EntropyGas,
    EnthalpyRefSolid, EnthalpyRefLiquid, EnthalpyRefGas,
    EntropyRefSolid, EntropyRefLiquid, EntropyRefGas,
    ExcessEnthalpyRefSolid, ExcessEnthalpyRefLiquid, ExcessEnthalpyRefGas,
    ExcessEntropyRefSolid, ExcessEntropyRefLiquid, ExcessEntropyRefGas,
    Excess_Solid_Enthalpy_Ref_Solid,
    Excess_Solid_Entropy_Ref_Solid,
    Excess_Liquid_Enthalpy_Ref_Liquid,
    Excess_Liquid_Entropy_Ref_Liquid,
    Excess_Gas_Enthalpy_Ref_Gas,
    Excess_Gas_Entropy_Ref_Gas
)
from .equilibrium.unifac import (
    DDBST_UNIFAC_assignments, 
    DDBST_MODIFIED_UNIFAC_assignments,
    DDBST_PSRK_assignments,
    UNIFACGroupCounts, 
    DortmundGroupCounts, 
    PSRKGroupCounts,
    NISTGroupCounts,
)
from .base import (PhaseHandle, PhaseTHandle, PhaseTPHandle,
                   display_asfunctor)
from .units_of_measure import chemical_units_of_measure
from .utils import copy_maybe, check_valid_ID
from . import functional as fn 
from ._phase import check_phase, valid_phases
from . import units_of_measure as thermo_units
from chemicals.utils import Z
from thermo.eos import IG, PR, SRK
from thermo import (
    TDependentProperty, TPDependentProperty,
    VaporPressure, 
    EnthalpyVaporization,
    SurfaceTension,
    SublimationPressure, EnthalpySublimation,
    VolumeSolid, VolumeLiquid, VolumeGas, VolumeSupercriticalLiquid,
    HeatCapacitySolid, HeatCapacityLiquid, HeatCapacityGas,
    ThermalConductivityLiquid, ThermalConductivityGas,
    ViscosityLiquid, ViscosityGas,
    PermittivityLiquid,
)

# from .solubility import SolubilityParameter
# from .lennard_jones import Stockmayer, MolecularDiameter
# from .environment import GWP, ODP, logP
# from .refractivity import refractive_index
# from .electrochem import conductivity

__all__ = ('Chemical',)

# %% Temporarily here

# TODO: Make a data table for new additions (not just for common sugars).
# Source: PubChem
sugar_solid_densities = {
    '50-99-7': 0.0001125975, 
    '57-48-7': 0.0001063495,
    '3458-28-4': 0.00011698,
    '25990-60-7': 9.84459e-05,
    '59-23-4': 0.000120104,   
}

class CompressibilityFactor:
    __slots__ = ('V',)
    
    def __init__(self, V):
        self.V = V
    
    def Z(self, T, P):
        return Z(T, P, self.V(T, P))
    
    def __repr__(self):
        return f"{type(self).__name__}(self.V)"

try:
    from CoolProp.CoolProp import PropsSI
except:
    pass
else:
    class CoolPropFreeEnergy:
        __slots__ = ('_ref', 'cache', 'name', 'CAS', 'MW')
        
        def __init__(self, CAS, name, MW):
            self.CAS = CAS
            self.name = name
            self.cache = (None, None)
            self._ref = PropsSI(self.name, 'T', 298.15, 'P', 101325., self.CAS) / 1000. * MW
            self.MW = MW
        
        def __call__(self, T, P):
            return PropsSI(self.name, 'T', T, 'P', P, self.CAS) * self.MW / 1000. - self._ref


# %% Filling missing properties

def get_chemical_data(chemical):
    getfield = getattr
    return {i:getfield(chemical, i, None) for i in chemical.__slots__}

def unpickle_chemical(chemical_data):
    chemical = object.__new__(Chemical)
    setfield = setattr
    for field, value in chemical_data.items():
        setfield(chemical, field, value)
    return chemical


# %% Representation
    
def chemical_identity(chemical, pretty=False):
    typeheader = f"{type(chemical).__name__}:"
    full_ID = f"{typeheader} {chemical.ID} (phase_ref={repr(chemical.phase_ref)})"
    phase = chemical.locked_state
    state = ' at ' + f"phase={repr(phase)}" if phase else ""
    return full_ID + state


# %% Initialize EOS
                         
def create_eos(eos, Tc, Pc, omega):
    try: return eos(T=298.15, P=101325., Tc=Tc, Pc=Pc, omega=omega)
    except: return IG(T=298.15, P=101325.)

# %% Resetting data

def reset_constant(chemical, var, value):
    getfield = getattr
    hasfield = hasattr
    setfield = setattr
    setfield(chemical, '_'+var, value)
    isa = isinstance
    for handle in _model_and_phase_handles: 
        handle = getfield(chemical, handle, None)
        if handle is None: continue
        if isa(handle, PhaseHandle):
            for phase, obj in handle:
                if hasfield(obj, var): setfield(obj, var, value)
        elif hasfield(handle, var): setfield(handle, var, value)

def reset_energy_constant(chemical, var, value):
    getfield = getattr
    hasfield = hasattr
    setfield = object.__setattr__
    setfield(chemical, '_'+var, value)
    isa = isinstance
    for handle in _energy_handles: 
        handle = getfield(chemical, handle, None)
        if handle is None: continue
        if isa(handle, PhaseHandle):
            for phase, obj in handle:
                if hasfield(obj, var): setfield(obj, var, value)
        elif hasfield(handle, var):
            setfield(handle, var, value)

def raise_helpful_handle_error(var, handle):
    if isinstance(handle, PhaseHandle):
        raise AttributeError(
            f"cannot set '{var}'; use `add_method` "
            f"to modify the thermodynamic properties for "
            f"each phase (e.g. {var}.l.add_method(...))")
    elif isinstance(handle, TDependentProperty):
        raise AttributeError(
            f"cannot set '{var}'; use `{var}.add_method` to "
            f"modify the thermodynamic property instead")
    else:
        raise Exception(
            'expected either a PhaseHandle or a TDependentProperty object; '
           f'got a {type(handle).__name__} object instead')

def raise_helpful_energy_functor_error(energy_functor):
    var = energy_functor.var
    raise AttributeError(f"cannot set '{var}'; use the `<Chemical>.reset_free_energies` "
                     "method to reset the free energy models with updated "
                     "chemical data")

# %%  Utilities

def as_chemical(chemical):
    return chemical if isinstance(chemical, Chemical) else Chemical(chemical)

# %% Chemical fields

_names = ('_CAS',
          '_InChI', 
          '_InChI_key',
          '_common_name',
          '_iupac_name',
          '_pubchemid', 
          '_smiles', 
          '_formula')

_groups = ('_Dortmund',
           '_UNIFAC', 
           '_PSRK',
           '_NIST')

_model_handles = ('_Psat', '_Hvap', '_sigma', '_epsilon', '_Hsub', '_Psub')
    
_phase_handles = ('_kappa', '_V', '_Cn', '_mu')

_energy_handles = ('_S_excess', '_H_excess', '_S', '_H')

_model_and_phase_handles = _model_handles + _phase_handles

_model_and_phase_properties = ('Psat', 'Hvap', 'sigma', 'epsilon',
                               'kappa', 'V', 'Cn', 'mu')

_handles = _model_and_phase_handles + _energy_handles

_data = ('_MW', '_Tm', '_Tb', '_Tt', '_Tc', '_Pt', '_Pc', '_Vc',
         '_Hf', '_S0', '_LHV', '_HHV', '_Hfus', '_Sfus', '_omega', '_dipole',
         '_similarity_variable', '_iscyclic_aliphatic', '_combustion',)

_functor_data = {'MW', 'Tm', 'Tb', 'Tt', 'Pt', 'Tc', 'Pc', 'Vc',
                 'Hfus', 'Sfus', 'omega', 'dipole',
                 'similarity_variable', 'iscyclic_aliphatic'}

_checked_properties = (
    'phase_ref', 'eos', 
    'S_excess', 'H_excess', 'mu', 'kappa', 'V', 'S', 
    'H', 'Cn', 'Psat', 'Hvap', 'sigma', 'epsilon', 
    'Dortmund', 'UNIFAC', 'PSRK', 'Hf', 'LHV', 'HHV',
    'combustion', *_functor_data
)

_chemical_fields = {'\n[Names]  ': _names,
                    '\n[Groups] ': _groups,
                    '\n[Data]   ': _data}

# %% Chemical

[docs] class Chemical: """ Creates a Chemical object which contains constant chemical properties, as well as thermodynamic and transport properties as a function of temperature and pressure. Parameters ---------- ID : str One of the following [-]: * Name, in IUPAC form or common form or an alias registered in PubChem * InChI name, prefixed by 'InChI=1S/' or 'InChI=1/' * InChI key, prefixed by 'InChIKey=' * PubChem CID, prefixed by 'PubChem=' * SMILES (prefix with 'SMILES=' to ensure smiles parsing) * CAS number cache : optional Whether or not to use cached chemicals and cache new chemicals. Other Parameters ---------------- search_ID : str, optional ID to search through database. Pass this key-word argument when you'd like to give a custom name to the chemical, but cannot find the chemical with that name. eos : GCEOS subclass, optional Equation of state class for solving thermodynamic properties. Defaults to Peng-Robinson. phase_ref : {'s', 'l', 'g'}, optional Reference phase of chemical. CAS : str, optional CAS number of chemical. phase: {'s', 'l' or 'g'}, optional Phase to set state of chemical. search_db=True: bool, optional Whether to search the data base for chemical. Cn : float or function(T), optional Molar heat capacity model [J/mol] as a function of temperature [K]. sigma : float or function(T), optional Surface tension model [N/m] as a function of temperature [K]. epsilon : float or function(T), optional Relative permittivity model [-] as a function of temperature [K]. Psat : float or function(T), optional Vapor pressure model [N/m] as a function of temperature [K]. Hvap : float or function(T), optional Heat of vaporization model [J/mol] as a function of temperature [K]. V : float or function(T, P), optional Molar volume model [m3/mol] as a function of temperature [K] and pressure [Pa]. mu : float or function(T, P), optional Dynamic viscosity model [Pa*s] as a function of temperature [K] and pressure [Pa]. kappa : float or function(T, P), optional Thermal conductivity model [W/m/K] as a function of temperature [K] and pressure [Pa]. Cp : float, optional Constant heat capacity model [J/g]. rho : float, optional Constant density model [kg/m3]. default=False : bool, optional Whether to default any missing chemical properties such as molar volume, heat capacity, surface tension, thermal conductivity, and molecular weight to that of water (on a weight basis). **data : float or str User data (e.g. Tb, formula, etc.). Examples -------- Chemical objects contain the same thermodynamic models and data as `thermo <https://thermo.readthedocs.io/>`_'s Chemical objects, but present an API more suitable for `BioSTEAM <https://biosteam.readthedocs.io/en/latest/>`_'s needs. Chemical objects contain pure component properties: >>> import thermosteam as tmo >>> # Initialize with an identifier >>> # (e.g. by name, CAS, InChI...) >>> Water = tmo.Chemical('Water') >>> # Water.show() >>> # Chemical: Water (phase_ref='l') >>> # [Names] CAS: 7732-18-5 >>> # InChI: H2O/h1H2 >>> # InChI_key: XLYOFNOQVPJJNP-U... >>> # common_name: water >>> # iupac_name: ('oxidane',) >>> # pubchemid: 962 >>> # smiles: O >>> # formula: H2O >>> # [Groups] Dortmund: <1H2O> >>> # UNIFAC: <1H2O> >>> # PSRK: <1H2O> >>> # NIST: <Empty> >>> # [Data] MW: 18.015 g/mol >>> # Tm: 273.15 K >>> # Tb: 373.12 K >>> # Tt: 273.16 K >>> # Tc: 647.1 K >>> # Pt: 611.65 Pa >>> # Pc: 2.2064e+07 Pa >>> # Vc: 5.5948e-05 m^3/mol >>> # Hf: -2.8582e+05 J/mol >>> # S0: 70 J/K/mol >>> # LHV: -44011 J/mol >>> # HHV: -0 J/mol >>> # Hfus: 6010 J/mol >>> # Sfus: None >>> # omega: 0.3443 >>> # dipole: 1.85 Debye >>> # similarity_variable: 0.16653 >>> # iscyclic_aliphatic: 0 >>> # combustion: {'H2O': 1.0} All fields shown are accessible: >>> Water.CAS '7732-18-5' Functional group identifiers (e.g. `Dortmund`, `UNIFAC`, `PSRK`) allow for the estimation of activity coefficients through group contribution methods. In other words, these attributes define the functional groups for thermodynamic equilibrium calculations: >>> Water.Dortmund <DortmundGroupCounts: 1H2O> Temperature (in Kelvin) and pressure (in Pascal) dependent properties can be computed: >>> # Vapor pressure (Pa) >>> Water.Psat(T=373.15) 101417.99 >>> # Surface tension (N/m) >>> Water.sigma(T=298.15) 0.0719 >>> # Molar volume (m^3/mol) >>> Water.V(phase='l', T=298.15, P=101325) 1.806...e-05 Note that the reference state of all chemicals is 25 degC and 1 atm: >>> (Water.T_ref, Water.P_ref) (298.15, 101325.0) >>> # Enthalpy at reference conditions (J/mol; without excess energies) >>> Water.H(T=298.15, phase='l') 0.0 Constant pure component properties are also available: >>> # Molecular weight (g/mol) >>> Water.MW 18.01528 >>> # Boiling point (K) >>> Water.Tb 373.124 Temperature dependent properties are managed by objects: >>> # Water.Psat >>> # VaporPressure(CASRN="7732-18-5", Tb=373.124, Tc=647.14, Pc=22048320.0, omega=0.344, extrapolation="AntoineAB|DIPPR101_ABC", method="HEOS_FIT") Phase dependent properties have attributes with model handles for each phase: >>> Water.V <PhaseTPHandle(phase, T, P=None) -> V [m^3/mol]> >>> # Water.V.l >>> # VolumeLiquid(CASRN="7732-18-5", MW=18.01528, Tb=373.124, Tc=647.14, Pc=22048320.0, Vc=5.6-05, Zc=0.2294727, omega=0.344, dipole=1.85, Psat=VaporPressure(CASRN="7732-18-5", Tb=373.124, Tc=647.14, Pc=22048320.0, omega=0.344, extrapolation="AntoineAB|DIPPR101_ABC", method="WAGNER_MCGARRY"), eos=[PR(Tc=647.14, Pc=22048320.0, omega=0.344, T=298.15, P=101325.0)], extrapolation="constant", method="VDI_PPDS", method_P="COSTALD_COMPRESSED", tabular_extrapolation_permitted=True) A new model can be added easily using `add_method`, for example: >>> def User_antoine_model(T): ... return 10.0**(10.116 - 1687.537 / (T - 42.98)) >>> # Water.Psat.add_method(f=User_antoine_model, Tmin=273.20, Tmax=473.20) >>> # Water.Psat >>> # VaporPressure(CASRN="7732-18-5", Tb=373.124, Tc=647.14, Pc=22048320.0, omega=0.344, extrapolation="AntoineAB|DIPPR101_ABC", method="USER_METHOD") The `add_method` method is a high level interface that even lets you create a constant model: >>> # Water.Cn.l.add_method(75.31) >>> # Water.Cn('l', 350) 75.31 .. Note:: Because no bounds were given, the model assumes it is valid across all temperatures and pressures. Choose what model to use through the `method` attribute: >>> # list(sorted(Water.Cn.l.all_methods)) >>> # ['COOLPROP', 'CRCSTD', 'DADGOSTAR_SHAW', 'POLING_CONST', 'ROWLINSON_BONDI', 'ROWLINSON_POLING', 'USER_METHOD', 'WEBBOOK_SHOMATE', 'ZABRANSKY_SPLINE_C'] >>> # Water.Cn.l.method = 'ZABRANSKY_SPLINE_C' .. note:: For details on the available methods and the capabilities of objects like VaporPressure and VolumeLiquid, visit `thermo's documentation <https://thermo.readthedocs.io/>`_. Attributes ---------- mu(phase, T, P) : Dynamic viscosity [Pa*s]. kappa(phase, T, P): Thermal conductivity [W/m/K]. V(phase, T, P): Molar volume [m^3/mol]. Cn(phase, T) : Molar heat capacity [J/mol/K]. Psat(T) : Vapor pressure [Pa]. Hvap(T) : Heat of vaporization [J/mol] sigma(T) : Surface tension [N/m]. epsilon(T) : Relative permittivity [-] S(phase, T, P) : Entropy [J/mol]. H(phase, T) : Enthalpy [J/mol]. S_excess(T, P) : Excess entropy [J/mol]. H_excess(T, P) : Excess enthalpy [J/mol]. """ __slots__ = ('_ID', '_locked_state', '_phase_ref', '_eos', '_aliases', *_names, *_groups, *_handles, *_data, '_N_solutes') #: [thermo.GEOS] Default EOS for pure components EOS_default = PR #: [float] Reference temperature in Kelvin T_ref = 298.15 #: [float] Reference pressure in Pascal P_ref = 101325. #: [float] Reference enthalpy in J/mol H_ref = 0. #: dict[str, Chemical] Cached chemicals chemical_cache = {} #: [bool] Wheather or not to search cache by default cache = False ### Creators ### def __new__(cls, ID, cache=None, *, search_ID=None, eos=None, phase_ref=None, CAS=None, default=False, phase=None, search_db=True, V=None, Cn=None, mu=None, Cp=None, rho=None, sigma=None, kappa=None, epsilon=None, Psat=None, Hvap=None, method=None, **data): chemical_cache = cls.chemical_cache if cache is None: cache = cls.cache if cache and ID in chemical_cache: if any([search_ID, eos, phase_ref, CAS, default, phase, V, Cn, mu, Cp, rho, sigma, kappa, epsilon, Psat, Hvap, data]): warn('cached chemical returned; additional parameters disregarded', RuntimeWarning) return chemical_cache[ID] ID = ID.strip() if not phase and len(ID) > 2 and ID[-2] == ',': phase = ID[-1] if phase not in valid_phases: raise ValueError(f'invalid phase {repr(phase)} encountered while parsing ID') ID = ID[:-2] search_ID = search_ID or ID if search_db: metadata = pubchem_db.search(search_ID) data['metadata'] = metadata self = cls.new(ID, metadata.CASs, eos, phase_ref, phase, free_energies=False, **data) else: self = cls.blank(ID, CAS, phase_ref, phase=phase, free_energies=False, **data) if phase: if mu: self._mu.add_method(mu) if Cn: self._Cn.add_method(Cn) if kappa: self._kappa.add_method(kappa) if V: self._V.add_method(V) else: multi_phase_items = (('mu', mu), ('Cn', Cn), ('kappa', kappa), ('Cp', Cp), ('rho', rho), ('V', V)) for i,j in multi_phase_items: if j: raise ValueError(f'must specify phase to set {i} model') if sigma: self._sigma.add_method(sigma) if epsilon: self._epsilon.add_method(epsilon) if Psat: self._Psat.add_method(Psat) if Hvap: self._Hvap.add_method(Hvap) if phase: MW = self._MW if Cp: self._Cn.add_method(Cp if MW is None else Cp * MW) if rho: self._V.add_method(fn.rho_to_V(rho, 1.) if MW is None else fn.rho_to_V(rho, MW)) if default: self.default() if cache: chemical_cache[ID] = self if len(chemical_cache) > 1000: for i in chemical_cache: del chemical_cache[i] break if method: self.set_method(method) self.reset_free_energies() return self def set_method(self, method): for name in _model_handles: model_handle = getattr(self, name) if method not in model_handle.all_methods: continue model_handle.method = method for name in _phase_handles: obj = getattr(self, name) if method in model_handle.all_methods: if isinstance(obj, PhaseHandle): for phase, model_handle in obj: model_handle.method = method else: obj.method = method if hasattr(model_handle, 'all_methods_P') and method in model_handle.all_methods_P: if isinstance(obj, PhaseHandle): for phase, model_handle in obj: model_handle.method_P = method else: obj.method_P = method def use_coolprop_free_energies(self): setattr = object.__setattr__ for name in ('_H', '_S'): obj = getattr(self, name) # Phase determined by coolprop cpfe = CoolPropFreeEnergy(self.CAS, name.strip('_'), self.MW) if isinstance(obj, PhaseHandle): for phase, model_handle in obj: setattr(obj, phase, cpfe) else: setattr(obj, phase, cpfe) # Excess energies are always included f = lambda T, P: 0. for name in ('_H_excess', '_S_excess'): obj = getattr(self, name) if isinstance(obj, PhaseHandle): for phase, model_handle in obj: setattr(obj, phase, f) else: setattr(obj, phase, f)
[docs] @classmethod def new(cls, ID, CAS, eos=None, phase_ref=None, phase=None, **data): """Create a new chemical from data without searching through the database, and load all possible models from given data.""" self = super().__new__(cls) self._ID = ID self.reset(CAS, eos, phase_ref, phase=phase, **data) return self
[docs] @classmethod def blank(cls, ID, CAS=None, phase_ref=None, phase=None, formula=None, aliases=None, synonyms=None, free_energies=True, **data): """ Return a new Chemical object without any thermodynamic models or data (unless provided). Parameters ---------- ID : str Chemical identifier. CAS : str, optional CAS number. If none provided, it defaults to the `ID`. phase_ref : str, optional Phase at the reference state (T=298.15, P=101325). phase : str, optional Phase to set state as a single phase chemical. **data : Any data to fill chemical with. Examples -------- >>> from thermosteam import Chemical >>> Substance = Chemical.blank('Substance') >>> Substance.show() Chemical: Substance (phase_ref='l') [Names] CAS: Substance InChI: None InChI_key: None common_name: None iupac_name: None pubchemid: None smiles: None formula: None [Groups] Dortmund: <Empty> UNIFAC: <Empty> PSRK: <Empty> NIST: <Empty> [Data] MW: None Tm: None Tb: None Tt: None Tc: None Pt: None Pc: None Vc: None Hf: None S0: 0 J/K/mol LHV: None HHV: None Hfus: None Sfus: None omega: None dipole: None similarity_variable: None iscyclic_aliphatic: None combustion: None """ self = super().__new__(cls) self._ID = ID self._eos = None self._UNIFAC = UNIFACGroupCounts() self._Dortmund = DortmundGroupCounts() self._PSRK = PSRKGroupCounts() self._NIST = NISTGroupCounts() self.aliases = aliases or () if synonyms: if isinstance(synonyms, str): self.aliases.add(synonyms) else: self.aliases.update(synonyms) setfield = setattr for i in _names: setfield(self, i, None) for i in _data: setfield(self, i, None) for i in _energy_handles: setfield(self, i, None) check_valid_ID(ID) self._phase_ref = phase_ref or phase self._CAS = CAS or ID for i,j in data.items(): setfield(self, '_' + i , j) if formula: self.formula = formula self._eos = create_eos(self.EOS_default, self._Tc, self._Pc, self._omega) TDependentProperty.RAISE_PROPERTY_CALCULATION_ERROR = False self._estimate_missing_properties(None) self._init_handles(CAS, self._MW, self._Tm, self._Tb, self._Pt, self._Tt, self._Tc, self._Pc, self.Zc, self._Vc, self._omega, self._dipole, self._similarity_variable, self._iscyclic_aliphatic, self._eos, self.has_hydroxyl, self._Hfus) self._locked_state = None self._estimate_missing_properties(self.Psat) if phase: self.at_state(phase, reset_free_energies=False) else: self._set_phase_ref(phase_ref, self._Tm, self._Tb) self._init_reactions(self._Hf, self._S0, self._LHV, self._HHV, self._combustion, self.atoms) if free_energies: self._init_energies(self._Cn, self._Hvap, self._Psat, self._Hfus, self._Sfus, self._Tm, self._Tb, self._eos, self._phase_ref, self._S0) if self._formula and self._Hf is not None: self.reset_combustion_data() TDependentProperty.RAISE_PROPERTY_CALCULATION_ERROR = True return self
@property def charge(self): """Charge of chemical as described in the chemical formula.""" return charge_from_formula(self.formula)
[docs] def copy(self, ID, CAS=None, **data): """ Return a copy of the chemical with a new ID. Examples -------- >>> import thermosteam as tmo >>> Glucose = tmo.Chemical('Glucose') >>> Mannose = Glucose.copy('Mannose') >>> assert Mannose.MW == Glucose.MW """ new = super().__new__(self.__class__) getfield = getattr setfield = setattr for field in self.__slots__: value = getfield(self, field, None) setfield(new, field, copy_maybe(value)) for field in _names[:-1]: setfield(new, field, None) new._ID = ID new._CAS = CAS or ID new._aliases = set() TDependentProperty.RAISE_PROPERTY_CALCULATION_ERROR = False new._init_energies(new.Cn, new.Hvap, new.Psat, new.Hfus, new.Sfus, new.Tm, new.Tb, new.eos, new.phase_ref, new.S0) TDependentProperty.RAISE_PROPERTY_CALCULATION_ERROR = True for i,j in data.items(): setfield(new, i , j) return new
__copy__ = copy def __reduce__(self): return unpickle_chemical, (get_chemical_data(self),) ### Helpful functionality ###
[docs] def rho(self, *args, **kwargs): """ Return density given thermal condition [kg/m^3]. Examples -------- >>> import thermosteam as tmo >>> Water = tmo.Chemical('Water', cache=True) >>> Water.rho('l', 298.15, 101325) 997.064 """ return fn.V_to_rho(self._V(*args, **kwargs), self._MW)
[docs] def Cp(self, *args, **kwargs): """ Return heat capacity given thermal condition [J/g/K]. Examples -------- >>> import thermosteam as tmo >>> Water = tmo.Chemical('Water', cache=True) >>> Water.Cp('l', 298.15, 101325) 4.181 """ return self._Cn(*args, **kwargs) / self._MW
[docs] def alpha(self, *args, **kwargs): """ Return thermal diffusivity given thermal condition [m^2/s]. Examples -------- >>> import thermosteam as tmo >>> Water = tmo.Chemical('Water', cache=True) >>> Water.alpha('l', 298.15, 101325) 1.425...-07 """ return fn.alpha(self._kappa(*args, **kwargs), self.rho(*args, **kwargs), self.Cp(*args, **kwargs) * 1000.)
[docs] def nu(self, *args, **kwargs): """ Return kinematic viscosity given thermal condition [m^2/s]. Examples -------- >>> import thermosteam as tmo >>> Water = tmo.Chemical('Water', cache=True) >>> Water.nu('l', 298.15, 101325) 8.928...e-07 """ return fn.mu_to_nu(self._mu(*args, **kwargs), self.rho(*args, **kwargs))
[docs] def Pr(self, *args, **kwargs): """ Return the Prandtl number given thermal condition [-]. Examples -------- >>> import thermosteam as tmo >>> Water = tmo.Chemical('Water', cache=True) >>> Water.Pr('l', 298.15, 101325) 6.261 """ return fn.Pr(self.Cp(*args, **kwargs) * 1000, self._kappa(*args, **kwargs), self._mu(*args, **kwargs))
[docs] def get_property(self, name, units, *args, **kwargs): """ Return property in requested units. Parameters ---------- name : str Name of stream property. units : str Units of measure. *args, **kwargs : Thermal condition. Examples -------- >>> import thermosteam as tmo >>> Water = tmo.Chemical('Water', cache=True) >>> Water.get_property('sigma', 'N/m', 300.) # Surface tension 0.07168 >>> Water.get_property('rho', 'g/cm3', 'l', 300., 101325) # Density 0.9965 """ value = getattr(self, name)(*args, **kwargs) units_dct = thermo_units.chemical_units_of_measure if name in units_dct: original_units = units_dct[name] else: raise ValueError(f"'{name}' is not thermodynamic property") return original_units.convert(value, units)
@property def phase_ref(self): """{'s', 'l', 'g'} Phase at 298 K and 101325 Pa.""" return self._phase_ref @phase_ref.setter def phase_ref(self, phase): if phase not in ('s', 'l', 'g'): raise ValueError("phase must be either 's', 'l', or 'g'") self._phase_ref = phase self.reset_free_energies() ### Unchangeable properties ### @property def ID(self): """[str] Identification of chemical.""" return self._ID @property def CAS(self): """[str] CAS number of chemical.""" return self._CAS ### Names ### @property def aliases(self): """set[str] User-defined aliases.""" return self._aliases @aliases.setter def aliases(self, aliases): self._aliases = set([aliases]) if isinstance(aliases, str) else set(aliases) synonyms = aliases @property def InChI(self): """[str] IUPAC International Chemical Identifier.""" return self._InChI @InChI.setter def InChI(self, InChI): self._InChI = str(InChI) @property def InChI_key(self): """[str] IUPAC International Chemical Identifier shorthand.""" return self._InChI_key @InChI_key.setter def InChI_key(self, InChI_key): self._InChI_key = str(InChI_key) @property def common_name(self): """[str] Common name identifier.""" return self._common_name @common_name.setter def common_name(self, common_name): self._common_name = str(common_name) @property def iupac_name(self): """[tuple[str]] Standard names as defined by IUPAC.""" return self._iupac_name @iupac_name.setter def iupac_name(self, iupac_name): if isinstance(iupac_name, str): self._iupac_name = (iupac_name,) else: self._iupac_name = iupac_name @property def pubchemid(self): """[str] Chemical identifier as defined by PubMed.""" return self._pubchemid @pubchemid.setter def pubchemid(self, pubchemid): self._pubchemid = str(pubchemid) @property def smiles(self): """[str] Chemical SMILES formula.""" return self._smiles @smiles.setter def smiles(self, smiles): self._smiles = str(smiles) @property def formula(self): """[str] Chemical atomic formula.""" return self._formula @formula.setter def formula(self, formula): self._formula = str(formula) self._MW = compute_molecular_weight(self.atoms) if self._Hf: self.reset_combustion_data() ### Functional groups ### @property def Dortmund(self): """[DortmundGroupCounts] Dictionary-like object with functional group numerical identifiers as keys and the number of groups as values.""" return self._Dortmund @property def UNIFAC(self): """[UNIFACGroupCounts] Dictionary-like object with functional group numerical identifiers as keys and the number of groups as values.""" return self._UNIFAC @property def PSRK(self): """[PSRKGroupCounts] Dictionary-like object with functional group numerical identifiers as keys and the number of groups as values.""" return self._PSRK @property def NIST(self): """[NISTGroupCounts] Dictionary-like object with functional group numerical identifiers as keys and the number of groups as values.""" return self._NIST ### Equations of state ### @property def eos(self): """[object] Instance for solving equations of state.""" return self._eos ### Phase/model handles ### @property def mu(self): """Dynamic viscosity [Pa*s].""" return self._mu @mu.setter def mu(self, value): raise_helpful_handle_error('mu', self._mu) @property def kappa(self): """Thermal conductivity [W/m/K].""" return self._kappa @kappa.setter def kappa(self, value): raise_helpful_handle_error('kappa', self._kappa) @property def V(self): """Molar volume [m^3/mol].""" return self._V @V.setter def V(self, value): raise_helpful_handle_error('V', self._V) @property def Cn(self): """Molar heat capacity [J/mol/K].""" return self._Cn @Cn.setter def Cn(self, value): raise_helpful_handle_error('Cn', self._Cn) @property def Psat(self): """Vapor pressure [Pa].""" return self._Psat @Psat.setter def Psat(self, value): raise_helpful_handle_error('Psat', self._Psat) @property def Hvap(self): """Heat of vaporization [J/mol].""" return self._Hvap @Hvap.setter def Hvap(self, value): raise_helpful_handle_error('Hvap', self._Hvap) @property def Hsub(self): """Heat of sublimation [J/mol].""" return self._Hsub @Hsub.setter def Hsub(self, value): raise_helpful_handle_error('Hsub', self._Hsub) @property def Psub(self): """Sublimation pressure [Pa].""" return self._Psub @Psub.setter def Psub(self, value): raise_helpful_handle_error('Psub', self._Psub) @property def sigma(self): """Surface tension [N/m].""" return self._sigma @sigma.setter def sigma(self, value): raise_helpful_handle_error('sigma', self._sigma) @property def epsilon(self): """Relative permittivity [-].""" return self._epsilon @epsilon.setter def epsilon(self, value): raise_helpful_handle_error('epsilon', self._epsilon) @property def S(self): """Entropy [J/mol].""" return self._S @S.setter def S(self, value): raise_helpful_energy_functor_error('S', self._S) @property def H(self): """Enthalpy [J/mol].""" return self._H @H.setter def H(self, value): raise_helpful_energy_functor_error('H', self._H) @property def S_excess(self): """Excess entropy [J/mol].""" return self._S_excess @S_excess.setter def S_excess(self, value): raise_helpful_energy_functor_error('S_excess', self._S_excess) @property def H_excess(self): """Excess enthalpy [J/mol].""" return self._H_excess @H_excess.setter def H_excess(self, value): raise_helpful_energy_functor_error('H_excess', self._H_excess) ### Data ### @property def MW(self): """Molecular weight [g/mol].""" return self._MW @MW.setter def MW(self, MW): if self._MW: raise AttributeError('cannot set molecular weight') reset_constant(self, 'MW', float(MW)) @property def Tm(self): """Normal melting temperature [K].""" return self._Tm @Tm.setter def Tm(self, Tm): reset_constant(self, 'Tm', float(Tm)) self.reset_free_energies() @property def Tb(self): """Normal boiling temperature [K].""" return self._Tb @Tb.setter def Tb(self, Tb): reset_constant(self, 'Tb', float(Tb)) self.reset_free_energies() @property def Pt(self): """Triple point pressure [Pa].""" return self._Pt @Pt.setter def Pt(self, Pt): self._Pt = float(Pt) @property def Tt(self): """Triple point temperature [K].""" return self._Tt @Tt.setter def Tt(self, Tt): self._Tt = float(Tt) @property def Tc(self): """Critical point temperature [K].""" return self._Tc @Tc.setter def Tc(self, Tc): reset_constant(self, 'Tc', float(Tc)) @property def Pc(self): """Critical point pressure [Pa].""" return self._Pc @Pc.setter def Pc(self, Pc): reset_constant(self, 'Pc', float(Pc)) @property def Vc(self): """Critical point molar volume [m^3/mol].""" return self._Vc @Vc.setter def Vc(self, Vc): reset_constant(self, 'Vc', float(Vc)) @property def Hfus(self): """Heat of fusion [J/mol].""" return self._Hfus @Hfus.setter def Hfus(self, Hfus): reset_energy_constant(self, 'Hfus', float(Hfus)) @property def Sfus(self): """Entropy of fusion [J/mol].""" return self._Sfus @Sfus.setter def Sfus(self, Sfus): reset_energy_constant(self, 'Sfus', float(Sfus)) @property def omega(self): """Acentric factor [-].""" return self._omega @omega.setter def omega(self, omega): reset_constant(self, 'omega', float(omega)) @property def dipole(self): """Dipole moment [Debye].""" return self._dipole @dipole.setter def dipole(self, dipole): reset_constant(self, 'dipole', float(dipole)) @property def similarity_variable(self): """Similarity variable [-].""" return self._similarity_variable @similarity_variable.setter def similarity_variable(self, similarity_variable): reset_constant(self, 'similarity_variable', float(similarity_variable)) @property def iscyclic_aliphatic(self): """Whether the chemical is cyclic-aliphatic.""" return self._iscyclic_aliphatic @iscyclic_aliphatic.setter def iscyclic_aliphatic(self, iscyclic_aliphatic): reset_constant(self, 'iscyclic_aliphatic', bool(iscyclic_aliphatic)) ### Reaction data ### @property def Hf(self): """Heat of formation at reference phase and temperature [J/mol].""" return self._Hf @Hf.setter def Hf(self, Hf): self._Hf = float(Hf) if self._formula: self.reset_combustion_data() @property def S0(self): return self._S0 @S0.setter def S0(self, S0): reset_energy_constant(self, 'S0', float(S0)) @property def LHV(self): """Lower heating value [J/mol].""" return self._LHV @LHV.setter def LHV(self, LHV): self._LHV = float(LHV) @property def HHV(self): """Higher heating value [J/mol].""" return self._HHV @HHV.setter def HHV(self, HHV): self._HHV = float(HHV) @property def combustion(self): """dict[str, int] Combustion reaction.""" return self._combustion @combustion.setter def combustion(self, combustion): self._combustion = dict(combustion) ### Computed data ### @property def Stiel_Polar(self): """[float] Stiel Polar factor for computing surface tension.""" Psat = self._Psat omega = self._omega Tc = self._Tc Pc = self._Pc if all([Psat, omega, Tc, Pc]): try: P_at_Tr_sixtenths = Psat(0.6 * Tc) except: P_at_Tr_sixtenths = None if P_at_Tr_sixtenths: Stiel_Polar = compute_Stiel_Polar(P_at_Tr_sixtenths, Pc, omega) else: Stiel_Polar = None else: Stiel_Polar = None return Stiel_Polar @property def Zc(self): """[float] Compressibility factor.""" critical_point = (self._Tc, self._Pc, self._Vc) return fn.Z(*critical_point) if all(critical_point) else None @property def has_hydroxyl(self): """[bool] Whether or not chemical contains a hydroxyl functional group, as determined by the Dortmund/UNIFAC/PSRK functional groups.""" for dct in (self._Dortmund, self._UNIFAC, self._PSRK): for n in (14, 15, 16, 81): if n in dct: return True return False @property def atoms(self): """dict[str: int] Atom-count pairs based on formula.""" formula = self._formula return get_atoms(formula).copy() if formula else {} @atoms.setter def atoms(self, atoms): """dict[str: int] Atom-count pairs based on formula.""" self.formula = atoms_to_Hill(atoms)
[docs] def get_combustion_reaction(self, chemicals=None, conversion=1.0): """Return a Reaction object defining the combustion of this chemical. If no combustion stoichiometry is available, return None.""" combustion = self._combustion if not combustion: return None if len(combustion) == 1: return None ID = self._ID combustion = combustion.copy() combustion[ID] = -1 return tmo.reaction.Reaction(combustion, ID, conversion, chemicals)
[docs] def get_phase(self, T=298.15, P=101325.): """Return phase of chemical at given thermal condition. Examples -------- >>> from thermosteam import Chemical >>> Water = Chemical('Water', cache=True) >>> Water.get_phase(T=400, P=101325) 'g' """ if self._locked_state: return self._locked_state if self._Tm and T <= self._Tm: return 's' if self._Psat and P <= self._Psat(T): return 'g' else: return 'l'
### Data solvers ###
[docs] def Tsat(self, P, Tguess=None, Tmin=None, Tmax=None, *, check_validity=True): """Return the saturated temperature (in Kelvin) given the pressure (in Pascal).""" Tb = self._Tb Psat = self._Psat if check_validity and P >= self._Pc: raise ValueError( 'pressure, P, cannot be above the supercritical pressure, Pc' ) if not Psat: return None if not Tmin: Tmin = Psat.Tmin + 1. if not Tmax: Tmax = Psat.Tmax - 1. if Tb: if P == 101325: return Tb else: Tguess = Tb elif not Tguess: Tguess = (Tmin + Tmax)/2.0 y0 = Psat(Tmin) - P y1 = Psat(Tmax) - P if y0 < 0. < y1: T = IQ_interpolation(lambda T: Psat(T) - P, Tmin, Tmax, y0, y1, Tguess, 1e-6, 1e-2, checkroot=False) else: T = aitken_secant(lambda T: Psat(T) - P, Tmin, Tmax, 1e-6, 1e-2, checkroot=False) return T
### Reinitializers ###
[docs] def reset(self, CAS, eos=None, phase_ref=None, smiles=None, InChI=None, InChI_key=None, pubchemid=None, iupac_name=None, common_name=None, formula=None, MW=None, Tm=None, Tb=None, Tc=None, Pc=None, Vc=None, omega=None, Tt=None, Pt=None, Hf=None, S0=None, LHV=None, combustion=None, HHV=None, Hfus=None, dipole=None, similarity_variable=None, iscyclic_aliphatic=None, aliases=None, synonyms=None, *, metadata=None, phase=None, free_energies=True, ): """ Reset all chemical properties. Parameters ---------- CAS : str CAS number of chemical to load. eos : optional Equation of state. The default is Peng Robinson. phase_ref : str, optional Phase reference. Defaults to the phase at 298.15 K and 101325 Pa. """ try: info = metadata or pubchem_db.search_CAS(CAS) except: pass else: if formula and info.formula and get_atoms(formula) != get_atoms(info.formula): raise RuntimeError( f'{self.ID} (CAS: {CAS}) formula from database, ' f'{info.formula}, does not match with user ' f'specification, {formula}') if MW and info.MW and MW != info.MW: raise RuntimeError( f'{self.ID} (CAS: {CAS}) molecular weight from database, ' f'{info.MW:.2f} g/mol, does not match with user ' f'specification, {MW:.2f} g/mol') smiles = smiles or info.smiles InChI = InChI or info.InChI InChI_key = InChI_key or info.InChI_key pubchemid = pubchemid or info.pubchemid iupac_name = iupac_name or info.iupac_name, common_name = common_name or info.common_name formula = formula or info.formula MW = MW or info.MW if formula and not MW: MW = compute_molecular_weight(formula) self._init_names(CAS, smiles, InChI, InChI_key, pubchemid, iupac_name, common_name, formula, aliases, synonyms) self._init_groups(InChI_key) if CAS == '56-81-5': # TODO: Make this part of data self._Dortmund = DortmundGroupCounts({2: 2, 3: 1, 14: 2, 81: 1}) atoms = self.atoms self._init_data(CAS, MW, Tm, Tb, Tc, Pc, Vc, omega, Pt, Tt, Hfus, dipole, atoms, similarity_variable, iscyclic_aliphatic) self._init_eos(eos, self._Tc, self._Pc, self._omega) TDependentProperty.RAISE_PROPERTY_CALCULATION_ERROR = False self._estimate_missing_properties(None) self._init_handles(CAS, self._MW, self._Tm, self._Tb, self._Pt, self._Tt, self._Tc, self._Pc, self.Zc, self._Vc, self._omega, self._dipole, self._similarity_variable, self._iscyclic_aliphatic, self._eos, self.has_hydroxyl, self._Hfus) self._locked_state = None self._estimate_missing_properties(self._Psat) if phase: self.at_state(phase, reset_free_energies=False) else: self._set_phase_ref(phase_ref, self._Tm, self._Tb) self._init_reactions(Hf, S0, LHV, HHV, combustion, atoms) if free_energies: self._init_energies(self._Cn, self._Hvap, self._Psat, self._Hfus, self._Sfus, self._Tm, self._Tb, self._eos, self._phase_ref, self._S0) if self._formula and self._Hf is not None: self.reset_combustion_data() TDependentProperty.RAISE_PROPERTY_CALCULATION_ERROR = True
def _set_phase_ref(self, phase_ref, Tm, Tb): if phase_ref: # "Liquid", "Solid", and "Gas" are also valid arguments phase_ref = phase_ref[0].lower() else: T_ref = self.T_ref if Tm and T_ref <= Tm: phase_ref = 's' elif Tb and T_ref >= Tb: phase_ref = 'g' else: phase_ref = 'l' self._phase_ref = phase_ref
[docs] def reset_combustion_data(self, method="Stoichiometry"): """Reset combustion data (LHV, HHV, and combustion attributes) based on the molecular formula and the heat of formation.""" cd = combustion_data(self.atoms, Hf=self._Hf, MW=self._MW, method=method) if not self._MW: self._MW = cd.MW self._LHV = - cd.LHV self._HHV = - cd.HHV self._combustion = cd.stoichiometry
[docs] def reset_free_energies(self): """Reset the `H`, `S`, `H_excess`, and `S_excess` functors.""" if not self._eos: self._eos = create_eos(self.EOS_default, self._Tc, self._Pc, self._omega) TDependentProperty.RAISE_PROPERTY_CALCULATION_ERROR = False self._init_energies(self._Cn, self._Hvap, self._Psat, self._Hfus, self._Sfus, self._Tm, self._Tb, self._eos, self._phase_ref, self._S0) TDependentProperty.RAISE_PROPERTY_CALCULATION_ERROR = True
### Initializers ### def _init_names(self, CAS, smiles, InChI, InChI_key, pubchemid, iupac_name, common_name, formula, aliases, synonyms): self._CAS = CAS self._smiles = smiles self._InChI = InChI self._InChI_key = InChI_key self._pubchemid = pubchemid self._iupac_name = iupac_name self._common_name = common_name self.aliases = aliases or () if synonyms: if isinstance(synonyms, str): self.aliases.add(synonyms) else: self.aliases.update(synonyms) self._formula = formula def _init_groups(self, InChI_key): if InChI_key in DDBST_UNIFAC_assignments: self._UNIFAC = DDBST_UNIFAC_assignments[InChI_key] else: self._UNIFAC = UNIFACGroupCounts() if InChI_key in DDBST_MODIFIED_UNIFAC_assignments: self._Dortmund = DDBST_MODIFIED_UNIFAC_assignments[InChI_key] else: self._Dortmund = DortmundGroupCounts() if InChI_key in DDBST_PSRK_assignments: self._PSRK = DDBST_PSRK_assignments[InChI_key] else: self._PSRK = PSRKGroupCounts() # TODO: Download data for NIST UNIFAC group assignments self._NIST = NISTGroupCounts() def _init_data(self, CAS, MW, Tm, Tb, Tc, Pc, Vc, omega, Pt, Tt, Hfus, dipole, atoms, similarity_variable, iscyclic_aliphatic): self._MW = MW self._Tm = Tm or normal_melting_point_temperature(CAS) self._Tb = Tb = Tb or normal_boiling_point_temperature(CAS) # Critical Point self._Tc = Tc = Tc or critical_point_temperature(CAS) self._Pc = Pc = Pc or critical_point_pressure(CAS) self._Vc = Vc or critical_point_volume(CAS) data = (Tb, Tc, Pc) self._omega = omega or acentric_factor(CAS) or (acentric_factor_LK(*data) if all(data) else None) # Triple point self._Pt = Pt or triple_point_pressure(CAS) self._Tt = Tt or triple_point_temperature(CAS) # Energy self._Hfus = heat_of_fusion(CAS) or 0. if Hfus is None else Hfus self._Sfus = None if Hfus is None or Tm is None else Hfus / Tm # Other self._dipole = dipole or dipole_moment(CAS) atoms = atoms or self.atoms if atoms and not similarity_variable: similarity_variable = compute_similarity_variable(atoms, MW) self._similarity_variable = similarity_variable self._iscyclic_aliphatic = iscyclic_aliphatic or False def _init_reactions(self, Hf, S0, LHV, HHV, combustion, atoms): Hvap_298K = None if Hf is None: Hvap_298K = self.Hvap(298.15) Hf = heat_of_formation(self._CAS, self._phase_ref, Hvap_298K, self.Hfus) if S0 is None: Hvap_298K = self.Hvap(298.15) if Hvap_298K is None else Hvap_298K Svap_298K = None if Hvap_298K is None else Hvap_298K / 298.15 S0 = absolute_entropy_of_formation(self._CAS, self._phase_ref, Svap_298K, self.Sfus) or 0. self._Hf = Hf self._S0 = S0 atoms = atoms or self.atoms if not all([LHV, HHV, combustion]) and atoms and Hf: cd = combustion_data(atoms, Hf=self._Hf, MW=self._MW, missing_handling='Ash') LHV = - cd.LHV HHV = - cd.HHV combustion = cd.stoichiometry self._LHV = LHV self._HHV = HHV self._combustion = combustion def _init_eos(self, eos, Tc, Pc, omega): self._eos = create_eos(eos or self.EOS_default, Tc, Pc, omega) def _init_handles(self, CAS, MW, Tm, Tb, Pt, Tt, Tc, Pc, Zc, Vc, omega, dipole, similarity_variable, iscyclic_aliphatic, eos, has_hydroxyl, Hfus): # Vapor pressure self._Psat = Psat = VaporPressure(CASRN=CAS, Tb=Tb, Tc=Tc, Pc=Pc, omega=omega) # Volume Vl = VolumeLiquid(MW=MW, Tb=Tb, Tc=Tc, Pc=Pc, Vc=Vc, Zc=Zc, omega=omega, dipole=dipole, Psat=Psat, CASRN=CAS, eos=[eos], has_hydroxyl=has_hydroxyl) Vml_Tm = Vl(Tm, 101325.) if Tm else None Vs = VolumeSolid(CAS, MW, Tt, Vml_Tm) Vg = VolumeGas(MW=MW, Tc=Tc, Pc=Pc, omega=omega, dipole=dipole, eos=[eos], CASRN=CAS) self._V = V = PhaseTPHandle('V', Vs, Vl, Vg, Tc) if CAS in sugar_solid_densities: Vs.add_method(sugar_solid_densities[CAS]) # Sensible heats Cns = HeatCapacitySolid(CASRN=CAS, similarity_variable=similarity_variable, MW=MW) Cng = HeatCapacityGas(CASRN=CAS, MW=MW, similarity_variable=similarity_variable, iscyclic_aliphatic=iscyclic_aliphatic) Cnl = HeatCapacityLiquid(CASRN=CAS, Tc=Tc, omega=omega, MW=MW, similarity_variable=similarity_variable, Cpgm=Cng) self._Cn = Cn = PhaseTHandle('Cn', Cns, Cnl, Cng, Tc) # Latent heats Zl = CompressibilityFactor(Vl) Zg = CompressibilityFactor(Vg) self._Hvap = Hvap = EnthalpyVaporization( CASRN=CAS, Tb=Tb, Tc=Tc, Pc=Pc, omega=omega, Zl=Zl, Zg=Zg, similarity_variable=similarity_variable, Psat=Psat ) self._Hsub = Hsub = EnthalpySublimation( CASRN=CAS, Tm=Tm, Tt=Tt, Cpg=Cng, Cps=Cns, Hvap=Hvap ) self._Psub = SublimationPressure( CASRN=CAS, Tt=Tt, Pt=Pt, Hsub_t=Hsub(Tm) if Hsub and Tm else None, ) # Viscosity mul = ViscosityLiquid(CASRN=CAS, MW=MW, Tm=Tm, Tc=Tc, Pc=Pc, Vc=Vc, omega=omega, Psat=Psat, Vml=V.l) mug = ViscosityGas(CASRN=CAS, MW=MW, Tc=Tc, Pc=Pc, Zc=Zc, dipole=dipole, Vmg=Vg) self._mu = mu = PhaseTPHandle('mu', mul, mul, mug, Tc) # Conductivity kappal = ThermalConductivityLiquid(CASRN=CAS, MW=MW, Tm=Tm, Tb=Tb, Tc=Tc, Pc=Pc, omega=omega, Hfus=Hfus) kappag = ThermalConductivityGas(CASRN=CAS, MW=MW, Tb=Tb, Tc=Tc, Pc=Pc, Vc=Vc, Zc=Zc, omega=omega, dipole=dipole, Vmg=V.g, Cpgm=Cn.g, mug=mu.g) self._kappa = PhaseTPHandle('kappa', kappal, kappal, kappag, Tc) # Surface tension Hvap_Tb = self._Hvap(Tb) if Tb else None self._sigma = SurfaceTension(CASRN=CAS, MW=MW, Tb=Tb, Tc=Tc, Pc=Pc, Vc=Vc, Zc=Zc, omega=omega, Stiel_Polar=self.Stiel_Polar, Hvap_Tb=Hvap_Tb, Vml=Vl, Cpl=Cnl, extrapolation='constant') # Other self._epsilon = PermittivityLiquid(CAS) def _estimate_missing_properties(self, Psat): # Melting temperature is a week function of pressure, # so assume the triple point temperature is the # melting temperature Tm = self._Tm Tt = self._Tt Pt = self._Pt Tc = self._Tc Pc = self._Pc Vc = self._Vc if not Tm and Tt: self._Tm = Tm = Tt if not Tt and Tm: self._Tt = Tt = Tm if not Pt and Tt and Psat: self._Pt = Pt = Psat(Tt) # Find missing critical property, if only two are given. critical_point = (Tc, Pc, Vc) N_values = sum([bool(i) for i in critical_point]) if N_values == 2: third_property = Ihmels(*critical_point) if not Tc: self._Tc = third_property elif not Pc: self._Pc = third_property elif not Vc: self._Vc = third_property Tb = self._Tb if not Tb and Pc is not None and 101325. < Pc: try: self._Tb = Tb = self.Tsat(101325) except: pass omega = self._omega if not omega and Pc and Tc: if Psat: P_at_Tr_seventenths = Psat(0.7 * Tc) if P_at_Tr_seventenths: omega = acentric_factor_definition(P_at_Tr_seventenths, Pc) elif not omega and Tb: omega = acentric_factor_LK(Tb, Tc, Pc) self._omega = omega def _init_energies(self, Cn, Hvap, Psat, Hfus, Sfus, Tm, Tb, eos, phase_ref, S0): # Reference P_ref = self.P_ref T_ref = self.T_ref H_ref = self.H_ref Tc = self._Tc single_phase = self._locked_state if isinstance(Cn, PhaseHandle): Cn_s = Cn.s Cn_l = Cn.l Cn_g = Cn.g has_Cns = bool(Cn_s) has_Cnl = bool(Cn_l) has_Cng = bool(Cn_g) elif single_phase: self._H = Enthalpy.functor(Cn, T_ref, H_ref) if single_phase == 'g': self._S = EntropyGas.functor(Cn, T_ref, P_ref, S0) else: self._S = Entropy.functor(Cn, T_ref, P_ref, S0) Cn_s = Cn_l = Cn_g = Cn has_Cns = has_Cnl = has_Cng = False if single_phase == 'l': has_Cnl = bool(Cn_l) elif single_phase == 's': has_Cns = bool(Cn_s) elif single_phase == 'g': has_Cng = bool(Cn_g) else: has_Cns = has_Cnl = has_Cng = False if any((has_Cns, has_Cnl, has_Cng)): if Hvap: Hvap_Tb = Hvap(Tb) if Tb else None Svap_Tb = Hvap_Tb / Tb if Hvap_Tb else None else: Hvap_Tb = Svap_Tb = None # Enthalpy and entropy integrals if phase_ref != 'l' and has_Cnl and (Tm and Tb): H_int_Tm_to_Tb_l = Cn_l.T_dependent_property_integral(Tm, Tb) S_int_Tm_to_Tb_l = Cn_l.T_dependent_property_integral_over_T(Tm, Tb) else: H_int_Tm_to_Tb_l = S_int_Tm_to_Tb_l = None if phase_ref == 's' and has_Cns and Tm: H_int_T_ref_to_Tm_s = Cn_s.T_dependent_property_integral(T_ref, Tm) S_int_T_ref_to_Tm_s = Cn_s.T_dependent_property_integral_over_T(T_ref, Tm) else: H_int_T_ref_to_Tm_s = S_int_T_ref_to_Tm_s = None if phase_ref == 'g' and has_Cng and Tb: H_int_Tb_to_T_ref_g = Cn_g.T_dependent_property_integral(Tb, T_ref) S_int_Tb_to_T_ref_g = Cn_g.T_dependent_property_integral_over_T(Tb, T_ref) else: H_int_Tb_to_T_ref_g = S_int_Tb_to_T_ref_g = None if phase_ref == 'l': if has_Cnl: if Tb: H_int_T_ref_to_Tb_l = Cn_l.T_dependent_property_integral(T_ref, Tb) S_int_T_ref_to_Tb_l = Cn_l.T_dependent_property_integral_over_T(T_ref, Tb) else: H_int_T_ref_to_Tb_l = S_int_T_ref_to_Tb_l = None if Tm: try: H_int_Tm_to_T_ref_l = Cn_l.T_dependent_property_integral(Tm, T_ref) S_int_Tm_to_T_ref_l = Cn_l.T_dependent_property_integral_over_T(Tm, T_ref) except: H_int_Tm_to_T_ref_l = S_int_Tm_to_T_ref_l = None else: H_int_Tm_to_T_ref_l = S_int_Tm_to_T_ref_l = None else: H_int_Tm_to_T_ref_l = S_int_Tm_to_T_ref_l = \ H_int_T_ref_to_Tb_l = S_int_T_ref_to_Tb_l = None # Excess data if isinstance(eos, IG): H_dep_ref_g = S_dep_ref_g = H_dep_ref_l = S_dep_ref_l = \ H_dep_T_ref_Pb = S_dep_T_ref_Pb = H_dep_Tb_Pb_g = H_dep_Tb_P_ref_g = \ S_dep_Tb_P_ref_g = S_dep_Tb_Pb_g = 0 else: if phase_ref == 'g': try: eos_phase_ref = eos.to_TP(T_ref, P_ref) H_dep_ref_g = eos_phase_ref.H_dep_g S_dep_ref_g = eos_phase_ref.S_dep_g except: H_dep_ref_g = S_dep_ref_g = 0. elif phase_ref == 'l': try: eos_T_ref_Pb = eos.to_TP(T_ref, P_ref) H_dep_ref_l = H_dep_T_ref_Pb = eos_T_ref_Pb.H_dep_l S_dep_ref_l = S_dep_T_ref_Pb = eos_T_ref_Pb.S_dep_l except: H_dep_ref_l = H_dep_T_ref_Pb = 0. S_dep_ref_l = S_dep_T_ref_Pb = 0. if Tb: try: eos_Tb_P_ref = eos.to_TP(Tb, P_ref) H_dep_Tb_Pb_g = H_dep_Tb_P_ref_g = eos_Tb_P_ref.H_dep_g S_dep_Tb_Pb_g = S_dep_Tb_P_ref_g = eos_Tb_P_ref.S_dep_g except: S_dep_Tb_Pb_g = S_dep_Tb_P_ref_g = H_dep_Tb_P_ref_g = \ H_dep_Tb_Pb_g = 0. else: S_dep_Tb_Pb_g = S_dep_Tb_P_ref_g = H_dep_Tb_P_ref_g = \ H_dep_Tb_Pb_g = 0. # Enthalpy and Entropy if single_phase: # Reference state does not matter because phase will not change self._H = Enthalpy.functor(Cn, T_ref, H_ref) if phase_ref == 'g': self._S = EntropyGas.functor(Cn, T_ref, P_ref, S0) else: self._S = Entropy.functor(Cn, T_ref, S0) # Excess energies if single_phase == 's': self._H_excess = Excess_Solid_Enthalpy_Ref_Solid.functor() self._S_excess = Excess_Solid_Entropy_Ref_Solid.functor() elif single_phase == 'l': self._H_excess = Excess_Liquid_Enthalpy_Ref_Liquid.functor() self._S_excess = Excess_Liquid_Entropy_Ref_Liquid.functor() elif single_phase == 'g': self._H_excess = Excess_Gas_Enthalpy_Ref_Gas.functor(eos, H_dep_ref_g) self._S_excess = Excess_Gas_Entropy_Ref_Gas.functor(eos, S_dep_ref_g) else: if phase_ref == 's': sdata = (Cn_s, T_ref, H_ref) ldata = (Cn_l, H_int_T_ref_to_Tm_s, Hfus, Tm, H_ref) gdata = (Cn_g, H_int_T_ref_to_Tm_s, Hfus, H_int_Tm_to_Tb_l, Hvap_Tb, Tb, H_ref) self._H = EnthalpyRefSolid(sdata, ldata, gdata, Tc) sdata = (Cn_s, T_ref, S0) ldata = (Cn_l, S_int_T_ref_to_Tm_s, Sfus, Tm, S0) gdata = (Cn_g, S_int_T_ref_to_Tm_s, Sfus, S_int_Tm_to_Tb_l, Svap_Tb, Tb, P_ref, S0) self._S = EntropyRefSolid(sdata, ldata, gdata, Tc) elif phase_ref == 'l': sdata = (Cn_s, H_int_Tm_to_T_ref_l, Hfus, Tm, H_ref) ldata = (Cn_l, T_ref, H_ref) gdata = (Cn_g, H_int_T_ref_to_Tb_l, Hvap_Tb, Tb, H_ref) self._H = EnthalpyRefLiquid(sdata, ldata, gdata, Tc) sdata = (Cn_s, S_int_Tm_to_T_ref_l, Sfus, Tm, S0) ldata = (Cn_l, T_ref, S0) gdata = (Cn_g, S_int_T_ref_to_Tb_l, Svap_Tb, Tb, P_ref, S0) self._S = EntropyRefLiquid(sdata, ldata, gdata, Tc) elif phase_ref == 'g': sdata = (Cn_s, H_int_Tb_to_T_ref_g, Hvap_Tb, H_int_Tm_to_Tb_l, Hfus, Tm, H_ref) ldata = (Cn_l, H_int_Tb_to_T_ref_g, Hvap_Tb, Tb, H_ref) gdata = (Cn_g, T_ref, H_ref) self._H = EnthalpyRefGas(sdata, ldata, gdata, Tc) sdata = (Cn_s, S_int_Tb_to_T_ref_g, Svap_Tb, S_int_Tm_to_Tb_l, Sfus, Tm, S0) ldata = (Cn_l, S_int_Tb_to_T_ref_g, Svap_Tb, Tb, S0) gdata = (Cn_g, T_ref, P_ref, S0) self._S = EntropyRefGas(sdata, ldata, gdata, Tc) # Excess energies if phase_ref == 's': self._H_excess = ExcessEnthalpyRefSolid((), (), (), Tc) self._S_excess = ExcessEntropyRefSolid((), (), (), Tc) elif phase_ref == 'l': gdata = (eos, H_dep_T_ref_Pb, H_dep_ref_l, H_dep_Tb_Pb_g) self._H_excess = ExcessEnthalpyRefLiquid((), (), gdata, Tc) gdata = (eos, S_dep_T_ref_Pb, S_dep_ref_l, S_dep_Tb_Pb_g) self._S_excess = ExcessEntropyRefLiquid((), (), gdata, Tc) elif phase_ref == 'g': ldata = (eos, H_dep_Tb_Pb_g, H_dep_Tb_P_ref_g) gdata = (eos, H_dep_ref_g) self._H_excess = ExcessEnthalpyRefGas((), ldata, gdata, Tc) ldata = (eos, S_dep_Tb_Pb_g, S_dep_Tb_P_ref_g) gdata = (eos, S_dep_ref_g) self._S_excess = ExcessEntropyRefGas((), ldata, gdata, Tc) else: self._H = self._S = self._S_excess = self._H_excess = None ### Filling missing values ###
[docs] def get_key_property_names(self): """Return the attribute names of key properties required to model a process.""" if not self._locked_state and self._phase_ref != 's': return ('V', 'S', 'H', 'Cn', 'Psat', 'Tb', 'Hvap') else: return ('V', 'S', 'H', 'Cn')
[docs] def default(self, properties=None): """ Default all `properties` with the chemical properties of water. If no `properties` given, all essential chemical properties that are missing are defaulted. `properties` which are still missing are returned as set. Parameters ---------- properties : Iterable[str], optional Names of chemical properties to default. Returns ------- missing_properties : set[str] Names of chemical properties that are still missing. Examples -------- >>> from thermosteam import Chemical >>> Substance = Chemical.blank('Substance') >>> missing_properties = Substance.default() >>> sorted(missing_properties) ['Dortmund', 'Hvap', 'PSRK', 'Pc', 'Psat', 'Pt', 'Tb', 'Tc', 'Tm', 'Tt', 'UNIFAC', 'Vc', 'dipole', 'iscyclic_aliphatic', 'omega', 'similarity_variable'] Note that missing properties does not include essential properties volume, heat capacity, and conductivity. """ if not properties: properties = self.get_missing_properties(properties) hasfield = hasattr # Default to Water property values if 'MW' in properties: self._MW = MW = 1 else: MW = self._MW if 'sigma' in properties: self._sigma.add_method(0.072055) if 'mu' in properties: mu = self._mu if hasfield(mu, 'l'): mu.l.add_method(0.00091272) elif not mu: mu.add_method(0.00091272) if 'V' in properties: V = self._V V_default = fn.rho_to_V(1000, MW) if hasfield(V, 'l'): V.l.add_method(V_default) elif not V: V.add_method(V_default) if 'kappa' in properties: kappa = self._kappa if hasfield(kappa, 'l'): kappa.l.add_method(0.5942) if not kappa: kappa.add_method(0.5942) if self._formula: if any([i in properties for i in ('HHV', 'LHV', 'combustion', 'Hf')]): if 'Hf' in properties: method = 'Dulong' else: method = 'Stoichiometry' stoichiometry = combustion_stoichiometry(self.atoms, MW=MW, missing_handling='Ash') try: cd = combustion_data(self.atoms, Hf=self._Hf, MW=MW, stoichiometry=stoichiometry, method=method) except: if 'Hf' in properties: self._Hf = 0. if 'HHV' in properties: self._HHV = 0. if 'LHV' in properties: self._LHV = 0. if 'combustion' in properties: self._combustion = stoichiometry else: if 'Hf' in properties: self._Hf = cd.Hf if 'HHV' in properties: self._HHV = - cd.HHV if 'LHV' in properties: self._LHV = - cd.LHV if 'combustion' in properties: self._combustion = stoichiometry else: if 'LHV' in properties: self._LHV = 0 if 'HHV' in properties: self._HHV = 0 if 'combustion' in properties: self._combustion = {} if 'Hf' in properties: self._Hf = 0 if 'epsilon' in properties: self._epsilon.add_method(0) if 'phase_ref' in properties: self._phase_ref = 'l' if 'eos' in properties: self._eos = create_eos(self.EOS_default, self._Tc, self._Pc, self._omega) if 'Cn' in properties: MW = self._MW Cn = self._Cn phase_ref = self._phase_ref getfield = getattr single_phase = self._locked_state if single_phase: Cn.add_method(4.18*MW) Cn_phase = Cn else: Cn_phase = getfield(Cn, phase_ref) Cn_phase.add_method(4.18*MW) self.reset_free_energies() if 'Hfus' in properties: self._Hfus = 0. if 'Sfus' in properties: self._Sfus = 0. if not getattr(self, '_H', None): self.reset_free_energies() missing = set(properties) missing.difference_update({'MW', 'CAS', 'Cn', 'Hf', 'sigma', 'mu', 'kappa', 'LHV', 'HHV', 'epsilon', 'H', 'S', 'H_excess', 'S_excess', 'phase_ref', 'combustion', 'Hfus', 'Sfus'}) return missing
[docs] def get_missing_properties(self, properties=None): """ Return a list all missing thermodynamic properties. Examples -------- >>> from thermosteam import Chemical >>> Substance = Chemical.blank('Substance', phase_ref='l') >>> sorted(Substance.get_missing_properties()) ['Cn', 'Dortmund', 'H', 'HHV', 'H_excess', 'Hf', 'Hfus', 'Hvap', 'LHV', 'MW', 'PSRK', 'Pc', 'Psat', 'Pt', 'S', 'S_excess', 'Sfus', 'Tb', 'Tc', 'Tm', 'Tt', 'UNIFAC', 'Vc', 'combustion', 'dipole', 'epsilon', 'iscyclic_aliphatic', 'kappa', 'mu', 'omega', 'sigma', 'similarity_variable'] """ getfield = getattr return [i for i in (properties or _checked_properties) if not getfield(self, i, None)]
[docs] def copy_models_from(self, other, names=None): """Copy models from other.""" if names: for i in names: if i not in _model_and_phase_properties: raise ValueError(f"{i} is not a valid model name; " "names must be a subset of " f"{_model_and_phase_properties}") names = ['_' + i for i in names] else: missing_handles = self.get_missing_properties(_model_and_phase_handles) other_missing_handles = other.get_missing_properties(_model_and_phase_handles) names = set(missing_handles).difference(other_missing_handles) getfield = getattr setfield = object.__setattr__ isa = isinstance phase = self._locked_state other_phase = other._locked_state for key in names: handle = getfield(self, key) other_handle = getfield(other, key) if isa(handle, TDependentProperty): if isa(other_handle, TDependentProperty): pass elif isa(other_handle, PhaseHandle): other_handle = getfield(other_handle, phase) else: raise RuntimeError(f"unexpected type '{type(other_handle).__name__}" f"for attribute '{key}'") new_handle = other_handle.copy() new_handle.CASRN = handle.CASRN setfield(self, key, new_handle) elif isa(handle, PhaseHandle): if isa(other_handle, TDependentProperty): old_handle = getfield(handle, other_phase) new_handle = other_handle.copy() new_handle.CASRN = old_handle.CASRN setfield(handle, other_phase, new_handle) elif isa(other_handle, PhaseHandle): for i, obj in handle: new_handle = getfield(other_handle, i).copy() new_handle.CASRN = obj.CASRN setfield(handle, i, new_handle) if {'_Cn', '_Hvap'}.intersection(names): self.reset_free_energies()
@property def locked_state(self): """[str] Constant phase of chemical.""" return self._locked_state @property def N_solutes(self): """[int] Number of molecules formed when solvated.""" return getattr(self, '_N_solutes', None) @N_solutes.setter def N_solutes(self, N_solutes): self._N_solutes = int(N_solutes)
[docs] def at_state(self, phase, copy=False, reset_free_energies=True): """ Set the state of chemical. Examples -------- >>> from thermosteam import Chemical >>> N2 = Chemical('N2') >>> N2.at_state(phase='g') >>> N2.H(298.15, None) # No longer a function of phase 0.0 """ if copy: new = self.copy(self._ID) new.at_state(phase) return new if phase: phase = phase[0].lower() else: raise ValueError(f"invalid phase {repr(phase)}") locked_state = self._locked_state if locked_state: if locked_state != phase: raise TypeError(f"{self}'s state is already locked") else: return else: lock_phase(self, phase) if reset_free_energies: self.reset_free_energies()
[docs] def show(self): """Print all specifications""" info = chemical_identity(self, pretty=True) for header, fields in _chemical_fields.items(): section = [] for field in fields: value = getattr(self, field) field = field.lstrip('_') if value is None: line = f"{field}: None" if callable(value): line = f"{display_asfunctor(value, name=field, var=field, show_var=False)}" else: if isinstance(value, (int, float)): line = f"{field}: {value:.5g}" units = chemical_units_of_measure.get(field, "") if units: line += f' {units}' else: value = str(value) line = f"{field}: {value}" if len(line) > 27: line = line[:27] + '...' section.append(line) if section: info += header + ("\n" + 9*" ").join(section) print(info)
_ipython_display_ = show def __str__(self): return self._ID def __repr__(self): return f"Chemical('{self}')"
def lock_phase(chemical, phase): check_phase(phase) getfield = getattr setfield = object.__setattr__ hasfield = hasattr for field in _phase_handles: phase_property = getfield(chemical, field) if hasfield(phase_property, phase): model_handle = getfield(phase_property, phase) setfield(chemical, field, model_handle) for field in _energy_handles: try: phase_property = getfield(chemical, field) except: continue if hasfield(phase_property, phase): functor = getfield(phase_property, phase) setfield(chemical, field, functor) # Reference state does not matter because phase will not change chemical._phase_ref = phase chemical._locked_state = phase # # Fire Safety Limits # self.Tflash = Tflash(CAS) # self.Tautoignition = Tautoignition(CAS) # self.LFL = LFL(CASRN=CAS, atoms=self.atoms, Hc=self.Hc) # self.UFL = UFL(CASRN=CAS, atoms=self.atoms, Hc=self.Hc) # # Chemical Exposure Limits # self.TWA = TWA(CASRN=CAS) # self.STEL = STEL(CASRN=CAS) # self.Ceiling = Ceiling(CASRN=CAS) # self.Skin = Skin(CASRN=CAS) # self.Carcinogen = Carcinogen(CASRN=CAS) # # Misc # self.dipole = dipole(CASRN=CAS) # Units of Debye # self.Stockmayer = Stockmayer(CASRN=CAS, Tm=self.Tm, Tb=self.Tb, Tc=self.Tc, # Zc=self.Zc, omega=self.omega) # # Environmental # self.GWP = GWP(CASRN=CAS) # self.ODP = ODP(CASRN=CAS) # self.logP = logP(CASRN=CAS) # # Analytical # self.RI, self.RIT = refractive_index(CASRN=CAS) # self.conductivity, self.conductivityT = conductivity(CASRN=CAS)