Skip to content

Commit

Permalink
make mixture models use phase handles to incorporate hooks from phase…
Browse files Browse the repository at this point in the history
… handles
  • Loading branch information
yoelcortes committed Aug 22, 2022
1 parent 078ff41 commit 547923e
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 91 deletions.
9 changes: 1 addition & 8 deletions thermosteam/_chemical.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,14 +424,7 @@ class Chemical:
Choose what model to use through the `method` attribute:
>>> list(sorted(Water.Cn.l.all_methods))
['CRCSTD',
'DADGOSTAR_SHAW',
'POLING_CONST',
'ROWLINSON_BONDI',
'ROWLINSON_POLING',
'USER_METHOD',
'WEBBOOK_SHOMATE',
'ZABRANSKY_SPLINE_C']
['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::
Expand Down
85 changes: 49 additions & 36 deletions thermosteam/base/phase_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
from ..utils import read_only, cucumber

__all__ = ('PhaseHandle',
'MockPhaseHandle',
'MockPhaseTHandle',
'MockPhaseTPHandle',
'PhaseTHandle',
'PhaseTPHandle',
'PhaseMixtureHandle',
'PhaseFunctorBuilder',
'PhaseTFunctorBuilder',
'PhaseTPFunctorBuilder')
Expand Down Expand Up @@ -56,64 +58,75 @@ def show(self):
print(self)
_ipython_display_ = show


# %% Pure component

class PhaseTHandle(PhaseHandle):
__slots__ = ()

def __call__(self, phase, T, P=None):
if self.Tc is not None and T > self.Tc: phase = 'g'
return getattr(self, phase)(T)


class PhaseTPHandle(PhaseHandle):
__slots__ = ()

def __call__(self, phase, T, P):
if self.Tc is not None and T > self.Tc: phase = 'g'
return getattr(self, phase)(T, P)


# %% Mixture

@cucumber
@read_only
@functor_lookalike
class PhaseMixtureHandle:
__slots__ = ('var', 's', 'l', 'g')
class MockPhaseHandle:
__slots__ = ('var', 'model')

def __init__(self, var, s, l, g):
def __init__(self, var, model):
setattr = object.__setattr__
setattr(self, 'var', var)
setattr(self, 's', s)
setattr(self, 'l', l)
setattr(self, 'g', g)
setattr(self, 'model', model)

def __call__(self, phase, z, T, P=None):
return getattr(self, phase)(z, T, P)

@property
def S(self): return self.s
def S(self): return self.model
@property
def L(self): return self.l
def L(self): return self.model
@property
def s(self): return self.model
@property
def l(self): return self.model
@property
def g(self): return self.model

def __iter__(self):
return iter((('s', self.s), ('l', self.l), ('g', self.g)))
return iter((('s', self.model), ('l', self.model), ('g', self.model)))

def __bool__(self):
return any((self.s, self.l, self.g))
return bool(self.model)

def copy(self):
return self.__class__(
self.var, self.s.copy(), self.l.copy(), self.g.copy(),
self.var, self.model.copy(),
)
__copy__ = copy

def show(self):
print(self)
_ipython_display_ = show


# %% Pure component

class PhaseTHandle(PhaseHandle):
__slots__ = ()

def __call__(self, phase, T, P=None):
if self.Tc is not None and T > self.Tc: phase = 'g'
return getattr(self, phase)(T)


class PhaseTPHandle(PhaseHandle):
__slots__ = ()

def __call__(self, phase, T, P):
if self.Tc is not None and T > self.Tc: phase = 'g'
return getattr(self, phase)(T, P)


class MockPhaseTHandle(MockPhaseHandle):
__slots__ = ()

def __call__(self, phase, T, P=None):
return self.model(T)


class MockPhaseTPHandle(MockPhaseHandle):
__slots__ = ()

def __call__(self, phase, T, P):
return self.model(T, P)

# %% Builders

Expand Down
112 changes: 100 additions & 12 deletions thermosteam/mixture/ideal_mixture_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,22 @@ class IdealTPMixtureModel:
>>> from thermosteam.mixture import IdealTPMixtureModel
>>> from thermosteam import Chemicals
>>> chemicals = Chemicals(['Water', 'Ethanol'])
>>> models = [i.V.l for i in chemicals]
>>> models = [i.V for i in chemicals]
>>> mixture_model = IdealTPMixtureModel(models, 'V')
>>> mixture_model
<IdealTPMixtureModel(mol, T, P) -> V [m^3/mol]>
<IdealTPMixtureModel(phase, mol, T, P) -> V [m^3/mol]>
>>> mixture_model([0.2, 0.8], 350, 101325)
5.376...e-05
"""
__slots__ = ('var', 'models',)
__slots__ = ('var', 'models')

def __init__(self, models, var):
self.models = tuple(models)
self.var = var

def __call__(self, mol, T, P):
return sum([j * i(T, P) for i, j in zip(self.models, mol) if j])
def __call__(self, phase, mol, T, P):
return sum([j * i(phase, T, P) for i, j in zip(self.models, mol) if j])

def __repr__(self):
return f"<{display_asfunctor(self)}>"
Expand Down Expand Up @@ -90,10 +90,10 @@ class IdealEntropyModel:
>>> from thermosteam import Chemicals
>>> import numpy as np
>>> chemicals = Chemicals(['Water', 'Ethanol'])
>>> models = [i.S.l for i in chemicals]
>>> models = [i.S for i in chemicals]
>>> mixture_model = IdealEntropyModel(models, 'S')
>>> mixture_model
<IdealEntropyModel(mol, T, P) -> S [J/K/mol]>
<IdealEntropyModel(phase, mol, T, P) -> S [J/K/mol]>
>>> mixture_model(np.array([0.2, 0.8]), 350, 101325)
160.3
Expand All @@ -102,9 +102,9 @@ class IdealEntropyModel:
__init__ = IdealTPMixtureModel.__init__
__repr__ = IdealTPMixtureModel.__repr__

def __call__(self, mol, T, P):
def __call__(self, phase, mol, T, P):
total_mol = mol.sum()
return sum([j * i(T, P) + j * log(j / total_mol) for i, j in zip(self.models, mol) if j])
return sum([j * i(phase, T, P) + j * log(j / total_mol) for i, j in zip(self.models, mol) if j])


class IdealTMixtureModel:
Expand All @@ -128,15 +128,56 @@ class IdealTMixtureModel:
--------
:class:`Mixture`
Examples
--------
>>> from thermosteam.mixture import IdealTMixtureModel
>>> from thermosteam import Chemicals
>>> chemicals = Chemicals(['Water', 'Ethanol'])
>>> models = [i.Cn for i in chemicals]
>>> mixture_model = IdealTMixtureModel(models, 'Cn')
>>> mixture_model
<IdealTMixtureModel(phase, mol, T, P=None) -> Cn [J/mol/K]>
>>> mixture_model([0.2, 0.8], 350)
84914.8703877987
"""
__slots__ = IdealTPMixtureModel.__slots__
__init__ = IdealTPMixtureModel.__init__
__repr__ = IdealTPMixtureModel.__repr__

def __call__(self, phase, mol, T, P=None):
return sum([j * i(phase, T) for i, j in zip(self.models, mol) if j])

class SinglePhaseIdealTMixtureModel:
"""
Create an SinglePhaseIdealTMixtureModel object that calculates mixture properties
based on the molar weighted sum of pure chemical properties.
Parameters
----------
models : Iterable[function(T, P)]
Chemical property functions of temperature and pressure.
var : str
Description of thermodynamic variable returned.
Notes
-----
:class:`Mixture` objects can contain IdealMixtureModel objects to establish
as mixture model for thermodynamic properties.
See also
--------
:class:`Mixture`
Examples
--------
>>> from thermosteam.mixture import IdealTMixtureModel
>>> from thermosteam import Chemicals
>>> chemicals = Chemicals(['Water', 'Ethanol'])
>>> models = [i.Psat for i in chemicals]
>>> mixture_model = IdealTMixtureModel(models, 'Psat')
>>> mixture_model = SinglePhaseIdealTMixtureModel(models, 'Psat')
>>> mixture_model
<IdealTMixtureModel(mol, T, P=None) -> Psat [Pa]>
<SinglePhaseIdealTMixtureModel(mol, T, P=None) -> Psat [Pa]>
>>> mixture_model([0.2, 0.8], 350)
84914.8703877987
Expand All @@ -148,6 +189,53 @@ class IdealTMixtureModel:
def __call__(self, mol, T, P=None):
return sum([j * i(T) for i, j in zip(self.models, mol) if j])

class SinglePhaseIdealTPMixtureModel:
"""
Create an IdealTPMixtureModel object that calculates mixture properties
based on the molar weighted sum of pure chemical properties.
Parameters
----------
models : Iterable[function(T, P)]
Chemical property functions of temperature and pressure.
var : str
Description of thermodynamic variable returned.
Notes
-----
:class:`Mixture` objects can contain IdealMixtureModel objects to establish
as mixture model for thermodynamic properties.
See also
--------
:class:`Mixture`
:func:`~.mixture_builders.ideal_mixture`
Examples
--------
>>> from thermosteam.mixture import SinglePhaseIdealTPMixtureModel
>>> from thermosteam import Chemicals
>>> chemicals = Chemicals(['Water', 'Ethanol'])
>>> models = [i.sigma for i in chemicals]
>>> mixture_model = SinglePhaseIdealTPMixtureModel(models, 'sigma')
>>> mixture_model
<SinglePhaseIdealTPMixtureModel(mol, T, P) -> sigma [N/m]>
>>> mixture_model([0.2, 0.8], 350, 101325)
5.376...e-05
"""
__slots__ = ('var', 'models')

def __init__(self, models, var):
self.models = tuple(models)
self.var = var

def __call__(self, mol, T, P):
return sum([j * i(T, P) for i, j in zip(self.models, mol) if j])

def __repr__(self):
return f"<{display_asfunctor(self)}>"

class IdealHvapModel:
__slots__ = ('chemicals',)
var = 'Hvap'
Expand All @@ -161,4 +249,4 @@ def __call__(self, mol, T, P=None):
if i and not j.locked_state
])

__repr__ = IdealTPMixtureModel.__repr__
__repr__ = IdealTPMixtureModel.__repr__
60 changes: 25 additions & 35 deletions thermosteam/mixture/mixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
from math import exp
from thermosteam import functional as fn
from .. import units_of_measure as thermo_units
from ..base import PhaseMixtureHandle
from ..base import PhaseHandle, MockPhaseTHandle, MockPhaseTPHandle
from .ideal_mixture_model import (
SinglePhaseIdealTMixtureModel,
IdealTMixtureModel,
IdealTPMixtureModel,
IdealEntropyModel,
Expand All @@ -25,32 +26,21 @@

# %% Functions for building mixture models

def group_handles_by_phase(phase_handles):
hasfield = hasattr
def create_mixture_model(chemicals, var, Model):
getfield = getattr
iscallable = callable
handles_by_phase = {'s': [],
'l': [],
'g': []}
for phase, handles in handles_by_phase.items():
for phase_handle in phase_handles:
if iscallable(phase_handle) and hasfield(phase_handle, phase):
prop = getfield(phase_handle, phase)
if not iscallable(prop): prop = phase_handle
else:
prop = phase_handle
handles.append(prop)
return handles_by_phase
isa = isinstance
handles = []
for chemical in chemicals:
obj = getfield(chemical, var)
if isa(obj, PhaseHandle):
phase_handle = obj
elif var in ('Cn', 'H'):
phase_handle = MockPhaseTHandle(var, obj)
else:
phase_handle = MockPhaseTPHandle(var, obj)
handles.append(phase_handle)
return Model(handles, var)

def build_ideal_PhaseMixtureHandle(chemicals, var, Model):
setfield = object.__setattr__
getfield = getattr
phase_handles = [getfield(i, var) for i in chemicals]
new = PhaseMixtureHandle.__new__(PhaseMixtureHandle)
for phase, handles in group_handles_by_phase(phase_handles).items():
setfield(new, phase, Model(handles, var))
setfield(new, 'var', var)
return new

# %% Energy balance

Expand Down Expand Up @@ -235,17 +225,17 @@ def from_chemicals(cls, chemicals,
chemicals = [(i if isa(i, Chemical) else Chemical(i)) for i in chemicals]
MWs = chemical_data_array(chemicals, 'MW')
getfield = getattr
Cn = build_ideal_PhaseMixtureHandle(chemicals, 'Cn', IdealTMixtureModel)
H = build_ideal_PhaseMixtureHandle(chemicals, 'H', IdealTMixtureModel)
S = build_ideal_PhaseMixtureHandle(chemicals, 'S', IdealEntropyModel)
H_excess = build_ideal_PhaseMixtureHandle(chemicals, 'H_excess', IdealTPMixtureModel)
S_excess = build_ideal_PhaseMixtureHandle(chemicals, 'S_excess', IdealTPMixtureModel)
mu = build_ideal_PhaseMixtureHandle(chemicals, 'mu', IdealTPMixtureModel)
V = build_ideal_PhaseMixtureHandle(chemicals, 'V', IdealTPMixtureModel)
kappa = build_ideal_PhaseMixtureHandle(chemicals, 'kappa', IdealTPMixtureModel)
Cn = create_mixture_model(chemicals, 'Cn', IdealTMixtureModel)
H = create_mixture_model(chemicals, 'H', IdealTMixtureModel)
S = create_mixture_model(chemicals, 'S', IdealEntropyModel)
H_excess = create_mixture_model(chemicals, 'H_excess', IdealTPMixtureModel)
S_excess = create_mixture_model(chemicals, 'S_excess', IdealTPMixtureModel)
mu = create_mixture_model(chemicals, 'mu', IdealTPMixtureModel)
V = create_mixture_model(chemicals, 'V', IdealTPMixtureModel)
kappa = create_mixture_model(chemicals, 'kappa', IdealTPMixtureModel)
Hvap = IdealHvapModel(chemicals)
sigma = IdealTMixtureModel([getfield(i, 'sigma') for i in chemicals], 'sigma')
epsilon = IdealTMixtureModel([getfield(i, 'epsilon') for i in chemicals], 'epsilon')
sigma = SinglePhaseIdealTMixtureModel([getfield(i, 'sigma') for i in chemicals], 'sigma')
epsilon = SinglePhaseIdealTMixtureModel([getfield(i, 'epsilon') for i in chemicals], 'epsilon')
return cls(rule, Cn, H, S, H_excess, S_excess,
mu, V, kappa, Hvap, sigma, epsilon, MWs, include_excess_energies)
else:
Expand Down

0 comments on commit 547923e

Please sign in to comment.