# -*- 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 numpy as np
import thermosteam as tmo
from . import equilibrium as eq
from ._chemical import Chemical
from ._chemicals import Chemicals
from .mixture import Mixture, IdealMixture
from .utils import read_only, cucumber
__all__ = ('Thermo', 'IdealThermo')
[docs]
@cucumber # Just means you can pickle it
@read_only
class Thermo:
"""
Create a Thermo object that defines a thermodynamic property package
Parameters
----------
chemicals : Iterable[:class:`~thermosteam.Chemical` or str]
Pure component chemical data.
mixture : :class:`~thermosteam.mixture.Mixture`, optional
Calculates mixture properties.
Gamma : :class:`~thermosteam.equilibrium.activity_coefficients.ActivityCoefficients` subclass, optional
Class for computing activity coefficients.
Phi : :class:`~thermosteam.equilibrium.fugacity_coefficients.FugacityCoefficients` subclass, optional
Class for computing fugacity coefficients.
PCF : :class:`~thermosteam.equilibrium.poyinting_correction_factors.PoyintingCorrectionFactors` subclass, optional
Class for computing poynting correction factors.
cache : optional
Whether or not to use cached chemicals.
skip_checks : bool, optional
Whether to skip checks for missing or invalid properties.
Examples
--------
Create a property package for water and ethanol:
>>> import thermosteam as tmo
>>> thermo = tmo.Thermo(['Ethanol', 'Water'], cache=True)
>>> thermo.show()
Thermo(
chemicals=CompiledChemicals([Ethanol, Water]),
mixture=IdealMixture(...
include_excess_energies=False
),
Gamma=DortmundActivityCoefficients,
Phi=IdealFugacityCoefficients,
PCF=MockPoyintingCorrectionFactors
)
Note that the Dortmund-UNIFAC is the default activity coefficient model.
The ideal-equilibrium property package (which assumes a value of 1 for all
activity coefficients) is also available:
>>> ideal = thermo.ideal()
>>> ideal.show()
IdealThermo(
chemicals=CompiledChemicals([Ethanol, Water]),
mixture=IdealMixture(...
include_excess_energies=False
),
)
Thermodynamic equilibrium results are affected by the choice of property package:
>>> # Ideal
>>> tmo.settings.set_thermo(ideal)
>>> stream = tmo.Stream('stream', Water=100, Ethanol=100)
>>> stream.vle(T=361, P=101325)
>>> stream.show()
MultiStream: stream
phases: ('g', 'l'), T: 361 K, P: 101325 Pa
flow (kmol/hr): (g) Ethanol 30
Water 16
(l) Ethanol 70
Water 84
>>> # Modified Roult's law:
>>> tmo.settings.set_thermo(thermo)
>>> stream = tmo.Stream('stream', Water=100, Ethanol=100)
>>> stream.vle(T=361, P=101325)
>>> stream.show()
MultiStream: stream
phases: ('g', 'l'), T: 361 K, P: 101325 Pa
flow (kmol/hr): (g) Ethanol 100
Water 100
Thermodynamic property packages are pickleable:
>>> tmo.utils.save(thermo, "Ethanol-Water Property Package")
>>> thermo = tmo.utils.load("Ethanol-Water Property Package")
>>> thermo.show()
Thermo(
chemicals=CompiledChemicals([Ethanol, Water]),
mixture=IdealMixture(...
include_excess_energies=False
),
Gamma=DortmundActivityCoefficients,
Phi=IdealFugacityCoefficients,
PCF=MockPoyintingCorrectionFactors
)
Attributes
----------
chemicals : Chemicals or Iterable[str]
Pure component chemical data.
mixture : Mixture, optional
Calculates mixture properties.
Gamma : ActivityCoefficients subclass, optional
Class for computing activity coefficients.
Phi : FugacityCoefficients subclass, optional
Class for computing fugacity coefficients.
PCF : PoyntingCorrectionFactor subclass, optional
Class for computing poynting correction factors.
"""
__slots__ = ('chemicals', 'mixture', 'Gamma', 'Phi', 'PCF', '_ideal', '_original_thermo')
def __init__(self, chemicals, mixture=None,
Gamma=None,
Phi=None,
PCF=None,
cache=None,
skip_checks=False):
if Gamma is None: Gamma = eq.DortmundActivityCoefficients
if Phi is None: Phi = eq.IdealFugacityCoefficients
if PCF is None: PCF = eq.MockPoyintingCorrectionFactors
if not isinstance(chemicals, Chemicals): chemicals = Chemicals(chemicals, cache)
if not mixture:
mixture = IdealMixture.from_chemicals(chemicals)
elif not isinstance(mixture, Mixture): # pragma: no cover
raise ValueError(f"mixture must be a '{Mixture.__name__}' object")
chemicals.compile(skip_checks=skip_checks)
issubtype = issubclass
if not issubtype(Gamma, eq.ActivityCoefficients): # pragma: no cover
raise ValueError(f"Gamma must be a '{eq.ActivityCoefficients.__name__}' subclass")
if not issubtype(Phi, eq.FugacityCoefficients): # pragma: no cover
raise ValueError(f"Phi must be a '{eq.FugacityCoefficients.__name__}' subclass")
if not issubtype(PCF, eq.PoyintingCorrectionFactors): # pragma: no cover
raise ValueError(f"PCF must be a '{eq.PoyintingCorrectionFactors.__name__}' subclass")
setattr = object.__setattr__
setattr(self, 'chemicals', chemicals)
setattr(self, 'mixture', mixture)
setattr(self, 'Gamma', Gamma)
setattr(self, 'Phi', Phi)
setattr(self, 'PCF', PCF)
setattr(self, '_ideal', None)
setattr(self, '_original_thermo', None)
def __enter__(self):
self._original_thermo = tmo.settings.get_thermo()
tmo.settings.set_thermo(self)
return self
def __exit__(self):
tmo.settings.set_thermo(self._original_thermo)
def extended(self, chemicals):
original_chemicals = self.chemicals
return self.subset([
*original_chemicals,
*[i for i in chemicals if i.ID not in original_chemicals],
])
def subset(self, chemicals):
if chemicals is self.chemicals: return self
isa = isinstance
if not isa(chemicals, Chemicals):
chemicals = Chemicals([self.as_chemical(i) for i in chemicals])
groups = [(name, index) for name, index in self.chemicals._index.items()
if isa(index, list)]
chemicals.compile(skip_checks=True)
CASs = self.chemicals.CASs
for name, index in groups:
group_CASs = [CASs[i] for i in index]
chemicals.define_group(name, [i for i in group_CASs if i in chemicals])
cls = self.__class__
new = cls.__new__(cls)
setattr = object.__setattr__
setattr(new, 'chemicals', chemicals)
setattr(new, 'mixture', self.mixture.from_chemicals(chemicals))
setattr(new, 'Gamma', self.Gamma)
setattr(new, 'Phi', self.Phi)
setattr(new, 'PCF', self.PCF)
setattr(new, '_ideal', None)
setattr(new, '_original_thermo', None)
return new
[docs]
def ideal(self):
"""Ideal thermodynamic property package."""
ideal = self._ideal
if not ideal:
ideal = IdealThermo.__new__(IdealThermo)
setattr = object.__setattr__
setattr(ideal, 'chemicals', self.chemicals)
setattr(ideal, 'mixture', self.mixture)
setattr(self, '_ideal', ideal)
setattr(ideal, '_original_thermo', None)
return ideal
[docs]
def as_chemical(self, chemical):
"""
Return chemical as a Chemical object.
Parameters
----------
chemical : str or Chemical
Name of chemical being retrieved.
Examples
--------
>>> import thermosteam as tmo
>>> thermo = tmo.Thermo(['Ethanol', 'Water'], cache=True)
>>> thermo.as_chemical('Water') is thermo.chemicals.Water
True
>>> thermo.as_chemical('Octanol') # Chemical not defined, so it will be created
Chemical('Octanol')
"""
isa = isinstance
if isa(chemical, str):
chemical = self.chemicals[chemical] if chemical in self.chemicals else Chemical(chemical)
elif not isa(chemical, Chemical): # pragma: no cover
raise ValueError(f"cannot convert '{type(chemical).__name__}' object to chemical")
return chemical
def __repr__(self):
return f"{type(self).__name__}(chemicals={self.chemicals}, mixture={self.mixture}, Gamma={self.Gamma.__name__}, Phi={self.Phi.__name__}, PCF={self.PCF.__name__})"
def show(self):
try:
mixture_info = self.mixture._info().replace('\n', '\n ')
except: # pragma: no cover
mixture_info = str(self.mixture)
print(f"{type(self).__name__}(\n"
f" chemicals={self.chemicals},\n"
f" mixture={mixture_info},\n"
f" Gamma={self.Gamma.__name__},\n"
f" Phi={self.Phi.__name__},\n"
f" PCF={self.PCF.__name__}\n"
")")
_ipython_display_ = show
@cucumber # Just means you can pickle it
@read_only
class IdealThermo:
"""
Create a Thermo object that defines a thermodynamic property package
Parameters
----------
chemicals : Iterable[:class:`~thermosteam.Chemical` or str]
Pure component chemical data.
mixture : :class:`~thermosteam.mixture.Mixture`, optional
Calculates mixture properties.
cache : optional
Whether or not to use cached chemicals.
skip_checks : bool, optional
Whether to skip checks for missing or invalid properties.
Attributes
----------
chemicals : Chemicals or Iterable[str]
Pure component chemical data.
mixture : Mixture, optional
Calculates mixture properties.
"""
__slots__ = ('chemicals', 'mixture', '_original_thermo')
Gamma = eq.IdealActivityCoefficients
Phi = eq.IdealFugacityCoefficients
PCF = eq.MockPoyintingCorrectionFactors
as_chemical = Thermo.as_chemical
__enter__ = Thermo.__enter__
__exit__ = Thermo.__exit__
def __init__(self, chemicals, mixture=None,
cache=None,
skip_checks=False):
if not isinstance(chemicals, Chemicals): chemicals = Chemicals(chemicals, cache)
if not mixture:
mixture = Mixture.from_chemicals(chemicals)
elif not isinstance(mixture, Mixture): # pragma: no cover
raise ValueError(f"mixture must be a '{Mixture.__name__}' object")
chemicals.compile(skip_checks=skip_checks)
setattr = object.__setattr__
setattr(self, 'chemicals', chemicals)
setattr(self, 'mixture', mixture)
setattr(self, '_original_thermo', None)
def subset(self, chemicals):
if chemicals is self.chemicals: return self
isa = isinstance
if not isa(chemicals, Chemicals):
chemicals = Chemicals([self.as_chemical(i) for i in chemicals])
groups = [(name, index) for name, index in self.chemicals._index.items()
if isinstance(index, list)]
chemicals.compile(skip_checks=True)
CASs = self.chemicals.CASs
for name, index in groups:
group_CASs = [CASs[i] for i in index]
chemicals.define_group(name, [i for i in group_CASs if i in chemicals])
cls = self.__class__
new = cls.__new__(cls)
setattr = object.__setattr__
setattr(new, 'chemicals', chemicals)
setattr(new, 'mixture', self.mixture.from_chemicals(chemicals))
setattr(new, '_original_thermo', self._original_thermo)
return new
def ideal(self):
"""Ideal thermodynamic property package."""
return self
def __repr__(self):
return f"{type(self).__name__}(chemicals={self.chemicals}, mixture={self.mixture})"
def show(self):
try:
mixture_info = self.mixture._info().replace('\n', '\n ')
except: # pragma: no cover
mixture_info = str(self.mixture)
print(f"{type(self).__name__}(\n"
f" chemicals={self.chemicals},\n"
f" mixture={mixture_info},\n"
")")
_ipython_display_ = show