Source code for thermosteam.equilibrium.vle

# -*- 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.
"""
"""
from scipy.optimize import shgo
import flexsolve as flx
from numba import njit
from warnings import warn
from ..base import SparseVector
from ..exceptions import InfeasibleRegion, NoEquilibrium
from . import binary_phase_fraction as binary
from .equilibrium import Equilibrium
from .dew_point import DewPoint
from .bubble_point import BubblePoint
from .fugacity_coefficients import IdealFugacityCoefficients
from .poyinting_correction_factors import MockPoyintingCorrectionFactors
from . import activity_coefficients as ac
from .. import functional as fn
from ..utils import Cache
import thermosteam as tmo
import numpy as np

__all__ = ('VLE', 'VLECache')

@njit(cache=True)
def xy(x, Ks):
    x[x < 0] = 1e-16
    x /= x.sum()
    y = x * Ks
    y /= y.sum()
    return x, y

def xVlogK_iter_2n(xVlogK, pcf_Psat_over_P, T, P, z, f_gamma, gamma_args, f_phi, n,
                  gas_conversion, liquid_conversion):
    xVlogK = xVlogK.copy()
    x = xVlogK[:n]
    Ks = np.exp(xVlogK[n+1:])
    x, y = xy(x, Ks)
    Ks[:] = pcf_Psat_over_P * f_gamma(x, T, *gamma_args) / f_phi(y, T, P)
    if gas_conversion or liquid_conversion:
        V = xVlogK[n]
        if V < 0.: V = 0.
        elif V > 1.: V = 1.
        V = binary.compute_phase_fraction_2N(z, Ks)
        if gas_conversion: 
            z = z + gas_conversion(material=y * V, T=T, P=P, phase='g')
            z[z <= 0] = 1e-16
            z /= z.sum()
        if liquid_conversion:
            z = z + liquid_conversion(material=x * (1 - V), T=T, P=P, phase='l')
            z[z <= 0] = 1e-16
            z /= z.sum()
    Ks[Ks < 1e-16] = 1e-16
    xVlogK[n] = V = binary.compute_phase_fraction_2N(z, Ks)
    xVlogK[:n] = z/(1. + V * (Ks - 1.))
    xVlogK[n+1:] = np.log(Ks)
    return xVlogK

def xVlogK_iter(
        xVlogK, pcf_Psat_over_P, T, P,
        z, z_light, z_heavy, f_gamma, gamma_args,
        f_phi, n, gas_conversion, liquid_conversion
    ):
    xVlogK = xVlogK.copy()
    x = xVlogK[:n]
    Ks = np.exp(xVlogK[n+1:])
    x, y = xy(x, Ks)
    Ks[:] = pcf_Psat_over_P * f_gamma(x, T, *gamma_args) / f_phi(y, T, P)
    V = xVlogK[n]
    if V < 0.: V = 0.
    elif V > 1.: V = 1.
    if gas_conversion or liquid_conversion:
        if gas_conversion: 
            z = z + gas_conversion(material=y * V, T=T, P=P, phase='g')
            z[z <= 0] = 1e-16
            z /= z.sum()
        if liquid_conversion:
            z = z + liquid_conversion(material=x * (1 - V), T=T, P=P, phase='l')
            z[z <= 0] = 1e-16
            z /= z.sum()
    Ks[Ks < 1e-16] = 1e-16
    xVlogK[n] = V = binary.solve_phase_fraction_Rashford_Rice(z, Ks, V, z_light, z_heavy)
    xVlogK[:n] = z / (1. + V * (Ks - 1.))
    xVlogK[n+1:] = np.log(Ks)
    return xVlogK

def set_flows(vapor_mol, liquid_mol, index, vapor_data, total_data):
    vapor_mol[index] = vapor_data
    liquid_mol[index] = total_data - vapor_data

[docs] class VLE(Equilibrium, phases='lg'): """ Create a VLE object that performs vapor-liquid equilibrium when called. Parameters ---------- imol=None : :class:`~thermosteam.indexer.MaterialIndexer`, optional Molar chemical phase data is stored here. thermal_condition=None : :class:`~thermosteam.ThermalCondition`, optional Temperature and pressure results are stored here. thermo=None : :class:`~thermosteam.Thermo`, optional Themodynamic property package for equilibrium calculations. Defaults to `thermosteam.settings.get_thermo()`. Examples -------- First create a VLE object: >>> from thermosteam import indexer, equilibrium, settings >>> settings.set_thermo(['Water', 'Ethanol', 'Methanol', 'Propanol'], cache=True) >>> imol = indexer.MolarFlowIndexer( ... l=[('Water', 304), ('Ethanol', 30)], ... g=[('Methanol', 40), ('Propanol', 1)]) >>> vle = equilibrium.VLE(imol) >>> vle VLE(imol=MolarFlowIndexer( g=[('Methanol', 40), ('Propanol', 1)], l=[('Water', 304), ('Ethanol', 30)]), thermal_condition=ThermalCondition(T=298.15, P=101325)) Equilibrium given vapor fraction and pressure: >>> vle(V=0.5, P=101325) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.49), ('Propanol', 0.8958)], l=[('Water', 177.3), ('Ethanol', 3.622), ('Methanol', 6.509), ('Propanol', 0.1042)]), thermal_condition=ThermalCondition(T=363.85, P=101325)) Equilibrium given temperature and pressure: >>> vle(T=363.88, P=101325) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 127.4), ('Ethanol', 26.41), ('Methanol', 33.54), ('Propanol', 0.8968)], l=[('Water', 176.6), ('Ethanol', 3.59), ('Methanol', 6.456), ('Propanol', 0.1032)]), thermal_condition=ThermalCondition(T=363.88, P=101325)) Equilibrium given enthalpy and pressure: >>> H = vle.thermo.mixture.xH(vle.imol, T=363.88, P=101325) >>> vle(H=H, P=101325) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 127.4), ('Ethanol', 26.41), ('Methanol', 33.54), ('Propanol', 0.8968)], l=[('Water', 176.6), ('Ethanol', 3.59), ('Methanol', 6.456), ('Propanol', 0.1032)]), thermal_condition=ThermalCondition(T=363.88, P=101325)) Equilibrium given entropy and pressure: >>> S = vle.thermo.mixture.xS(vle.imol, T=363.88, P=101325) >>> vle(S=S, P=101325) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 127.4), ('Ethanol', 26.41), ('Methanol', 33.54), ('Propanol', 0.8968)], l=[('Water', 176.6), ('Ethanol', 3.59), ('Methanol', 6.456), ('Propanol', 0.1032)]), thermal_condition=ThermalCondition(T=363.88, P=101325)) Equilibrium given vapor fraction and temperature: >>> vle(V=0.5, T=363.88) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.49), ('Propanol', 0.8958)], l=[('Water', 177.3), ('Ethanol', 3.622), ('Methanol', 6.509), ('Propanol', 0.1042)]), thermal_condition=ThermalCondition(T=363.88, P=101431)) Equilibrium given enthalpy and temperature: >>> vle(H=H, T=363.88) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 127.4), ('Ethanol', 26.41), ('Methanol', 33.54), ('Propanol', 0.8968)], l=[('Water', 176.6), ('Ethanol', 3.59), ('Methanol', 6.456), ('Propanol', 0.1032)]), thermal_condition=ThermalCondition(T=363.88, P=101325)) Non-partitioning heavy and gaseous chemicals also affect VLE. Calculation are repeated with non-partitioning chemicals: >>> from thermosteam import indexer, equilibrium, settings, Chemical >>> O2 = Chemical('O2', phase='g') >>> Glucose = Chemical('Glucose', phase='l', default=True) >>> settings.set_thermo(['Water', 'Ethanol', 'Methanol', 'Propanol', O2, Glucose], cache=True) >>> imol = indexer.MolarFlowIndexer( ... l=[('Water', 304), ('Ethanol', 30), ('Glucose', 5)], ... g=[('Methanol', 40), ('Propanol', 1), ('O2', 10)]) >>> vle = equilibrium.VLE(imol) >>> vle(T=363.88, P=101325) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 159.5), ('Ethanol', 27.65), ('Methanol', 35.63), ('Propanol', 0.9337), ('O2', 10)], l=[('Water', 144.5), ('Ethanol', 2.352), ('Methanol', 4.369), ('Propanol', 0.0663), ('Glucose', 5)]), thermal_condition=ThermalCondition(T=363.88, P=101325)) >>> vle(V=0.5, P=101325) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.52), ('Propanol', 0.8957), ('O2', 10)], l=[('Water', 177.3), ('Ethanol', 3.618), ('Methanol', 6.478), ('Propanol', 0.1043), ('Glucose', 5)]), thermal_condition=ThermalCondition(T=362.47, P=101325)) >>> H = vle.thermo.mixture.xH(vle.imol, T=363.88, P=101325) >>> vle(H=H, P=101325) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 127.4), ('Ethanol', 26.42), ('Methanol', 33.58), ('Propanol', 0.8968), ('O2', 10)], l=[('Water', 176.6), ('Ethanol', 3.583), ('Methanol', 6.421), ('Propanol', 0.1032), ('Glucose', 5)]), thermal_condition=ThermalCondition(T=362.51, P=101325)) >>> S = vle.thermo.mixture.xS(vle.imol, T=363.88, P=101325) >>> vle(S=S, P=101325) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 128.2), ('Ethanol', 26.45), ('Methanol', 33.63), ('Propanol', 0.8979), ('O2', 10)], l=[('Water', 175.8), ('Ethanol', 3.548), ('Methanol', 6.365), ('Propanol', 0.1021), ('Glucose', 5)]), thermal_condition=ThermalCondition(T=362.54, P=101325)) >>> vle(V=0.5, T=363.88) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.49), ('Propanol', 0.8958), ('O2', 10)], l=[('Water', 177.3), ('Ethanol', 3.622), ('Methanol', 6.509), ('Propanol', 0.1042), ('Glucose', 5)]), thermal_condition=ThermalCondition(T=363.88, P=106841)) >>> vle(H=H, T=363.88) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 126.7), ('Ethanol', 26.38), ('Methanol', 33.49), ('Propanol', 0.8958), ('O2', 10)], l=[('Water', 177.3), ('Ethanol', 3.622), ('Methanol', 6.51), ('Propanol', 0.1042), ('Glucose', 5)]), thermal_condition=ThermalCondition(T=363.88, P=106842)) >>> vle(S=S, T=363.88) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 128.1), ('Ethanol', 26.45), ('Methanol', 33.6), ('Propanol', 0.8978), ('O2', 10)], l=[('Water', 175.9), ('Ethanol', 3.555), ('Methanol', 6.399), ('Propanol', 0.1022), ('Glucose', 5)]), thermal_condition=ThermalCondition(T=363.88, P=106562)) The presence of a non-partitioning gaseous chemical will result in some evaporation, even if the tempeture is below the saturated bubble point: >>> from thermosteam import indexer, equilibrium, settings, Chemical >>> O2 = Chemical('O2', phase='g') >>> settings.set_thermo(['Water', O2], cache=True) >>> imol = indexer.MolarFlowIndexer( ... l=[('Water', 30)], ... g=[('O2', 10)]) >>> vle = equilibrium.VLE(imol) >>> vle(T=300., P=101325) >>> vle VLE(imol=MolarFlowIndexer( g=[('Water', 0.3617), ('O2', 10)], l=[('Water', 29.64)]), thermal_condition=ThermalCondition(T=300.00, P=101325)) """ __slots__ = ( 'method', # [str] Method for solving equilibrium. '_T', # [float] Temperature [K]. '_P', # [float] Pressure [Pa]. '_H_hat', # [float] Specific enthalpy [kJ/kg]. '_S_hat', # [float] Specific entropy [kJ/K/kg]. '_V', # [float] Molar vapor fraction. '_dew_point', # [DewPoint] Solves for dew point. '_bubble_point', # [BubblePoint] Solves for bubble point. '_y', # [1d array] Vapor composition. '_z_last', # tuple[1d array] Last bulk composition. '_phi', # [FugacityCoefficients] Estimates fugacity coefficients of gas. '_pcf', # [PoyintingCorrectionFactors] Estimates the PCF of a liquid. '_gamma', # [ActivityCoefficients] Estimates activity coefficients of a liquid. '_liquid_mol', # [1d array] Liquid molar data. '_vapor_mol', # [1d array] Vapor molar data. '_phase_data', # tuple[str, 1d array] Phase-data pairs. '_K', # [1d array] Partition coefficients. '_v', # [1d array] Molar vapor data in equilibrium. '_index', # [1d array] Index of chemicals in equilibrium. '_F_mass', # [float] Total mass data. '_chemical', # [Chemical] Single chemical in equilibrium. '_mol_vle', # [1d array] Moles of chemicals in VLE calculations. '_N', # [int] Number of chemicals in equilibrium. '_z', # [1d array] Molar composition of chemicals in equilibrium '_z_norm', # [1d array] Normalized molar composition of chemicals in equilibrium '_z_light', # [1d array] Molar composition of light chemicals not included in equilibrium calculation. '_z_heavy', # [1d array] Molar composition of heavy chemicals not included in equilibrium calculation. '_nonzero', # [1d array(bool)] Chemicals present in the mixture '_F_mol', # [float] Total moles of chemicals (accounting for dissociation). '_F_mol_vle', # [float] Total moles in equilibrium. '_F_mol_light', # [float] Total moles of gas chemicals not included in equilibrium calculation. '_F_mol_heavy', # [float] Total moles of heavy chemicals not included in equilibrium calculation. '_dmol_vle', '_dF_mol', ) maxiter = 20 T_tol = 5e-8 P_tol = 1. H_hat_tol = 1e-6 S_hat_tol = 1e-6 V_tol = 1e-6 K_tol = 1e-6 y_tol = 1e-8 default_method = 'fixed-point' def __init__(self, imol=None, thermal_condition=None, thermo=None): self.method = self.default_method self._T = self._P = self._H_hat = self._V = 0 super().__init__(imol, thermal_condition, thermo) self._z_last = None self._nonzero = None self._index = ()
[docs] def __call__(self, *, T=None, P=None, V=None, H=None, S=None, x=None, y=None, gas_conversion=None, liquid_conversion=None): """ Perform vapor-liquid equilibrium. Parameters ---------- T : float, optional Operating temperature [K]. P : float, optional Operating pressure [Pa]. V : float, optional Molar vapor fraction. H : float, optional Enthalpy [kJ/hr]. S : float, optional Entropy [kJ/hr/K] x : float, optional Molar composition of liquid (for binary mixtures). y : float, optional Molar composition of vapor (for binary mixtures). Notes ----- You may only specify two of the following parameters: P, H, T, V, x, and y. Additionally, If x or y is specified, the other parameter must be either P or T (e.g., x and V is invalid). """ if gas_conversion or liquid_conversion: if gas_conversion: if isinstance(gas_conversion, tmo.KineticReaction): gas_conversion = gas_conversion.conversion_handle(tmo.Stream(thermo=self.thermo)) elif isinstance(gas_conversion, (tmo.Reaction, tmo.ReactionItem)): gas_conversion = gas_conversion.conversion_handle(None) if liquid_conversion: if isinstance(liquid_conversion, tmo.KineticReaction): liquid_conversion = liquid_conversion.conversion_handle(tmo.Stream(thermo=self.thermo)) elif isinstance(liquid_conversion, (tmo.Reaction, tmo.ReactionItem)): liquid_conversion = liquid_conversion.conversion_handle(None) if x is not None or y is not None: raise ValueError( "can not pass either 'x' or 'y' arguments with reactions preset" ) ### Decide what kind of equilibrium to run ### T_spec = T is not None P_spec = P is not None V_spec = V is not None H_spec = H is not None S_spec = S is not None x_spec = x is not None y_spec = y is not None N_specs = (T_spec + P_spec + V_spec + H_spec + S_spec + x_spec + y_spec) assert N_specs == 2, ("must pass two and only two of the following " "specifications: T, P, V, H, S, x, y") # Run equilibrium if T_spec: if P_spec: try: self.set_thermal_condition(T, P, gas_conversion, liquid_conversion) except NoEquilibrium: thermal_condition = self._thermal_condition thermal_condition.T = T thermal_condition.P = P elif V_spec: try: self.set_TV(T, V, gas_conversion, liquid_conversion) except NoEquilibrium: thermal_condition = self._thermal_condition thermal_condition.T = T elif H_spec: self.set_TH(T, H, gas_conversion, liquid_conversion) elif S_spec: self.set_TS(T, S, gas_conversion, liquid_conversion) elif x_spec: self.set_Tx(T, np.asarray(x)) else: # y_spec self.set_Ty(T, np.asarray(y)) elif P_spec: if V_spec: try: self.set_PV(P, V, gas_conversion, liquid_conversion) except NoEquilibrium: thermal_condition = self._thermal_condition thermal_condition.P = P elif H_spec: try: self.set_PH(P, H, gas_conversion, liquid_conversion) except NoEquilibrium: thermal_condition = self._thermal_condition thermal_condition.P = P elif S_spec: try: self.set_PS(P, S, gas_conversion, liquid_conversion) except: try: self.set_PS(P, S, gas_conversion, liquid_conversion) except NoEquilibrium: thermal_condition = self._thermal_condition thermal_condition.P = P elif x_spec: self.set_Px(P, np.asarray(x)) else: # y_spec self.set_Py(P, np.asarray(y)) elif S_spec: # pragma: no cover if y_spec: raise NotImplementedError('specification S and y is invalid') elif x_spec: raise NotImplementedError('specification S and x is invalid') elif H_spec: raise NotImplementedError('specification H and S is invalid') else: # V_spec raise NotImplementedError('specification V and S not implemented') elif H_spec: # pragma: no cover if y_spec: raise NotImplementedError('specification H and y is invalid') elif x_spec: raise NotImplementedError('specification H and x is invalid') else: # V_spec raise NotImplementedError('specification V and H not implemented') elif V_spec: # pragma: no cover if y_spec: raise ValueError("specification V and y is invalid") else: # x_spec raise ValueError("specification V and x is invalid") else: # pragma: no cover raise ValueError("can only pass either 'x' or 'y' arguments, not both")
### Single component equilibrium case ### def _set_thermal_condition_chemical(self, T, P): chemical = self._chemical if T >= chemical.Tc: self._liquid_mol[self._index] = 0 self._vapor_mol[self._index] = self._mol_vle else: # Either liquid or gas Psat = chemical.Psat(T) tol = 1e-3 if P < Psat - tol: self._liquid_mol[self._index] = 0 self._vapor_mol[self._index] = self._mol_vle elif P > Psat + tol: self._liquid_mol[self._index] = self._mol_vle self._vapor_mol[self._index] = 0 def _set_TV_chemical(self, T, V): # Set vapor fraction self._T = self._thermal_condition.T = self._chemical.Psat(T) self._vapor_mol[self._index] = V * self._mol_vle self._liquid_mol[self._index] = self._mol_vle - self._vapor_mol[self._index] def _set_PV_chemical(self, P, V): # Set vapor fraction self._T = self._thermal_condition.T = self._chemical.Tsat(P, check_validity=False) self._vapor_mol[self._index] = self._mol_vle * V self._liquid_mol[self._index] = self._mol_vle - self._vapor_mol[self._index] def _set_PH_chemical(self, P, H): mol = self._mol_vle index = self._index vapor_mol = self._vapor_mol liquid_mol = self._liquid_mol thermo = self._thermo phase_data = self._phase_data mixture = thermo.mixture chemical = self._chemical # Set temperature in equilibrium self._T = self._thermal_condition.T = T = chemical.Tsat(P, check_validity=False) # Check if super heated vapor vapor_mol[index] = mol liquid_mol[index] = 0 H_dew = mixture.xH(phase_data, T, P) if H >= H_dew: self._thermal_condition.T = mixture.xsolve_T_at_HP(phase_data, H, T, P) return # Check if subcooled liquid vapor_mol[index] = 0 liquid_mol[index] = mol H_bubble = mixture.xH(phase_data, T, P) if H <= H_bubble: self._thermal_condition.T = mixture.xsolve_T_at_HP(phase_data, H, T, P) return # Adjust vapor fraction accordingly V = (H - H_bubble)/(H_dew - H_bubble) vapor_mol[index] = mol*V liquid_mol[index] = mol - vapor_mol[index] def _set_TH_chemical(self, T, H): index = self._index mol = self._mol_vle vapor_mol = self._vapor_mol liquid_mol = self._liquid_mol mixture = self._thermo.mixture phase_data = self._phase_data # Set Pressure in equilibrium self._thermal_condition.P = P = self._chemical.Psat(T) # Check if super heated vapor vapor_mol[index] = mol liquid_mol[index] = 0 H_dew = mixture.xH(phase_data, T, P) if H >= H_dew: raise NotImplementedError('cannot solve for pressure yet') self._thermal_condition.P = mixture.xsolve_P_at_HT(phase_data, H, T, P) return # Check if subcooled liquid vapor_mol[index] = 0 liquid_mol[index] = mol H_bubble = mixture.xH(phase_data, T, P) if H <= H_bubble: raise NotImplementedError('cannot solve for pressure yet') self._thermal_condition.P = mixture.xsolve_P_at_HT(phase_data, H, T, P) return # Adjust vapor fraction accordingly V = (H - H_bubble)/(H_dew - H_bubble) vapor_mol[index] = mol*V liquid_mol[index] = mol - vapor_mol[index] def _set_PS_chemical(self, P, S): mol = self._mol_vle index = self._index vapor_mol = self._vapor_mol liquid_mol = self._liquid_mol thermo = self._thermo phase_data = self._phase_data mixture = thermo.mixture chemical = self._chemical # Set temperature in equilibrium self._T = self._thermal_condition.T = T = chemical.Tsat(P, check_validity=False) # Check if super heated vapor vapor_mol[index] = mol liquid_mol[index] = 0 S_dew = mixture.xS(phase_data, T, P) if S >= S_dew: self._thermal_condition.T = mixture.xsolve_T_at_SP(phase_data, S, T, P) return # Check if subcooled liquid vapor_mol[index] = 0 liquid_mol[index] = mol S_bubble = mixture.xS(phase_data, T, P) if S <= S_bubble: self._thermal_condition.T = mixture.xsolve_T_at_SP(phase_data, S, T, P) return # Adjust vapor fraction accordingly V = (S - S_bubble)/(S_dew - S_bubble) vapor_mol[index] = mol*V liquid_mol[index] = mol - vapor_mol[index] def _set_TS_chemical(self, T, S): index = self._index mol = self._mol_vle vapor_mol = self._vapor_mol liquid_mol = self._liquid_mol mixture = self._thermo.mixture phase_data = self._phase_data # Set Pressure in equilibrium self._thermal_condition.P = P = self._chemical.Psat(T) # Check if super heated vapor vapor_mol[index] = mol liquid_mol[index] = 0 S_dew = mixture.xS(phase_data, T, P) if S >= S_dew: raise NotImplementedError('cannot solve for pressure yet') self._thermal_condition.P = mixture.xsolve_P_at_ST(phase_data, S, T, P) return # Check if subcooled liquid vapor_mol[index] = 0 liquid_mol[index] = mol S_bubble = mixture.xS(phase_data, T, P) if S <= S_bubble: raise NotImplementedError('cannot solve for pressure yet') self._thermal_condition.P = mixture.xsolve_P_at_ST(phase_data, S, T, P) return # Adjust vapor fraction accordingly V = (S - S_bubble)/(S_dew - S_bubble) vapor_mol[index] = mol*V liquid_mol[index] = mol - vapor_mol[index] def _lever_rule(self, x, y): split_frac = (self._z[0]-x[0])/(y[0]-x[0]) if not -0.00001 < split_frac < 1.00001: raise InfeasibleRegion('phase composition') if split_frac > 1: split_frac = 1 elif split_frac < 0: split_frac = 0 self._vapor_mol[self._index] = v = self._F_mol * split_frac * y self._liquid_mol[self._index] = self._mol_vle - v def set_Tx(self, T, x): self._setup() assert self._N == 2, 'number of species in equilibrium must be 2 to specify x' self._thermal_condition.P, y = self._bubble_point.solve_Py(x, T) self._lever_rule(x, y) def set_Px(self, P, x): self._setup() assert self._N == 2, 'number of species in equilibrium must be 2 to specify x' self._thermal_condition.T, y = self._bubble_point.solve_Ty(x, P) self._lever_rule(x, y) def set_Ty(self, T, y): self._setup() assert self._N == 2, 'number of species in equilibrium must be 2 to specify y' self._thermal_condition.P, x = self._dew_point.solve_Px(y, T) self._lever_rule(x, y) def set_Py(self, P, y): self._setup() assert self._N == 2, 'number of species in equilibrium must be 2 to specify y' self._thermal_condition.T, x = self._dew_point.solve_Tx(y, P) self._lever_rule(x, y) def set_thermal_condition(self, T, P, gas_conversion=None, liquid_conversion=None): self._setup(gas_conversion, liquid_conversion) thermal_condition = self._thermal_condition self._T = thermal_condition.T = T self._P = thermal_condition.P = P if self._N == 0: return if self._N == 1: return self._set_thermal_condition_chemical(T, P) # Check if there is equilibrium if gas_conversion: P_dew, dz_dew, x_dew, _ = self._dew_point.solve_Px(self._z, T, gas_conversion) if P <= P_dew and not self._F_mol_heavy: self._vapor_mol[self._index] = self._mol_vle + dz_dew * self._F_mol_vle self._liquid_mol[self._index] = 0 return else: P_dew, x_dew = self._dew_point.solve_Px(self._z, T) if P <= P_dew and not self._F_mol_heavy: self._vapor_mol[self._index] = self._mol_vle self._liquid_mol[self._index] = 0 return dz_dew = None if liquid_conversion: P_bubble, dz_bubble, y_bubble, _ = self._bubble_point.solve_Py(self._z, T, liquid_conversion) if P >= P_bubble and not self._F_mol_light: self._vapor_mol[self._index] = 0 self._liquid_mol[self._index] = self._mol_vle + dz_bubble * self._F_mol_vle return else: P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T) if P >= P_bubble and not self._F_mol_light: self._vapor_mol[self._index] = 0 self._liquid_mol[self._index] = self._mol_vle return dz_bubble = None # Guess composition in the vapor is a # weighted average of bubble/dew points dP = (P_bubble - P_dew) V = (P - P_dew) / dP if dP > 1. else 0.5 self._refresh_K(V, y_bubble, x_dew, dz_bubble, dz_dew) v = self._solve_v(T, P, gas_conversion, liquid_conversion) mol_vle = self._mol_vle + self._dmol_vle if (gas_conversion or liquid_conversion) else self._mol_vle set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle) def set_TV(self, T, V, gas_conversion=None, liquid_conversion=None): self._setup(gas_conversion, liquid_conversion) thermal_condition = self._thermal_condition thermal_condition.T = self._T = T if self._N == 0: raise RuntimeError('no chemicals present to perform VLE') if self._N == 1: return self._set_TV_chemical(T, V) if self._F_mol_heavy and V == 1.: V = 1. - 1e-3 if self._F_mol_light and V == 0.: V = 1e-3 if V == 1: if gas_conversion: P_dew, dz, y, x_dew = self._dew_point.solve_Px(self._z, T, gas_conversion) self._vapor_mol[self._index] = self._mol_vle + dz * self._F_mol_vle else: P_dew, x_dew = self._dew_point.solve_Px(self._z, T) self._vapor_mol[self._index] = self._mol_vle self._liquid_mol[self._index] = 0 thermal_condition.P = P_dew elif V == 0: if liquid_conversion: P_bubble, dz, y_bubble, x = self._bubble_point.solve_Py(self._z, T) self._liquid_mol[self._index] = self._mol_vle + dz * self._F_mol_vle else: P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T) self._liquid_mol[self._index] = self._mol_vle self._vapor_mol[self._index] = 0 thermal_condition.P = P_bubble else: if liquid_conversion: P_bubble, dz_bubble, y_bubble, _ = self._bubble_point.solve_Py(self._z, T, liquid_conversion) else: P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T) dz_bubble = None if gas_conversion: P_dew, dz_dew, y, x_dew = self._dew_point.solve_Px(self._z, T, gas_conversion) else: P_dew, x_dew = self._dew_point.solve_Px(self._z, T, gas_conversion) dz_dew = None self._refresh_K(V, y_bubble, x_dew, dz_bubble, dz_dew) if self._F_mol_light: P_bubble = 0.1 * self._bubble_point.Pmax + 0.9 * P_bubble if self._F_mol_heavy: P_dew = 0.1 * self._bubble_point.Pmin + 0.9 * P_dew V_bubble = self._V_err_at_P(P_bubble, 0., gas_conversion, liquid_conversion) if V_bubble > V: F_mol = self._F_mol mol = self._mol_vle if liquid_conversion: mol = mol + self._dmol_vle F_mol = F_mol + self._dF_mol F_mol_vapor = F_mol * V v = y_bubble * F_mol_vapor mask = v > mol v[mask] = mol[mask] P = P_bubble else: V_dew = self._V_err_at_P(P_dew, 0., gas_conversion, liquid_conversion) if V_dew < V: F_mol = self._F_mol mol = self._mol_vle if gas_conversion: mol = mol + self._dmol_vle F_mol = F_mol + self._dF_mol l = x_dew * F_mol * (1. - V) mask = l > mol l[mask] = mol[mask] v = mol - l P = P_dew else: P = flx.IQ_interpolation( self._V_err_at_P, P_bubble, P_dew, V_bubble - V, V_dew - V, self._P, self.P_tol, self.V_tol, (V, gas_conversion, liquid_conversion), checkiter=False, checkbounds=False, maxiter=self.maxiter, ) v = self._v mol = self._mol_vle if gas_conversion or liquid_conversion: mol = mol + self._dmol_vle self._P = thermal_condition.P = P set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol) if liquid_conversion or gas_conversion: try: self._H_hat = ( self.mixture.xH(self._phase_data, T, P) + (self.chemicals.Hf * mol).sum() ) / self._F_mass except: pass else: try: self._H_hat = self.mixture.xH(self._phase_data, T, P) / self._F_mass except: pass def set_TH(self, T, H, gas_conversion=None, liquid_conversion=None): self._setup(gas_conversion, liquid_conversion) if self._N == 0: raise RuntimeError('no chemicals present to perform VLE') if self._N == 1: return self._set_TH_chemical(T, H) self._T = T index = self._index mol = self._mol_vle vapor_mol = self._vapor_mol liquid_mol = self._liquid_mol phase_data = self._phase_data # Check if super heated vapor P_dew, x_dew = self._dew_point.solve_Px(self._z, T) if self._F_mol_heavy: P_dew = 0.5 * P_dew + 0.5 * self._bubble_point.Pmin vapor_mol[index] = mol liquid_mol[index] = 0 H_dew = self.mixture.xH(phase_data, T, P_dew) dH_dew = (H - H_dew) if dH_dew >= 0: raise NotImplementedError('cannot solve for pressure yet') # Check if subcooled liquid P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T) if self._F_mol_light: P_bubble = 2 * P_bubble vapor_mol[index] = 0 liquid_mol[index] = mol H_bubble = self.mixture.xH(phase_data, T, P_bubble) dH_bubble = (H - H_bubble) if dH_bubble <= 0: raise NotImplementedError('cannot solve for pressure yet') # Guess overall vapor fraction, and vapor flow rates V = dH_bubble/(H_dew - H_bubble) # Guess composition in the vapor is a weighted average of boiling points self._refresh_K(V, y_bubble, x_dew) F_mass = self._F_mass H_hat = H/F_mass P = flx.IQ_interpolation( self._H_hat_err_at_P, P_bubble, P_dew, H_bubble/F_mass - H_hat, H_dew/F_mass - H_hat, self._P, self.P_tol, self.H_hat_tol, (H_hat,), checkiter=False, checkbounds=False, maxiter=self.maxiter, ) self._P = self._thermal_condition.P = P self._thermal_condition.T = T def set_TS(self, T, S, gas_conversion=None, liquid_conversion=None): self._setup(gas_conversion, liquid_conversion) if self._N == 0: raise RuntimeError('no chemicals present to perform VLE') if self._N == 1: return self._set_TS_chemical(T, S) self._T = T index = self._index mol = self._mol_vle vapor_mol = self._vapor_mol liquid_mol = self._liquid_mol phase_data = self._phase_data # Check if super heated vapor P_dew, x_dew = self._dew_point.solve_Px(self._z, T) if self._F_mol_heavy: P_dew = 0.5 * P_dew + 0.5 * self._bubble_point.Pmin vapor_mol[index] = mol liquid_mol[index] = 0 S_dew = self.mixture.xS(phase_data, T, P_dew) dS_dew = (S - S_dew) if dS_dew >= 0: raise NotImplementedError('cannot solve for pressure yet') # Check if subcooled liquid P_bubble, y_bubble = self._bubble_point.solve_Py(self._z, T) if self._F_mol_light: P_bubble = 2 * P_bubble vapor_mol[index] = 0 liquid_mol[index] = mol S_bubble = self.mixture.xS(phase_data, T, P_bubble) dS_bubble = (S - S_bubble) if dS_bubble <= 0: raise NotImplementedError('cannot solve for pressure yet') # Guess overall vapor fraction, and vapor flow rates V = dS_bubble/(S_dew - S_bubble) # Guess composition in the vapor is a weighted average of boiling points self._refresh_K(V, y_bubble, x_dew) F_mass = self._F_mass S_hat = S/F_mass P = flx.IQ_interpolation( self._S_hat_err_at_P, P_bubble, P_dew, S_bubble/F_mass - S_hat, S_dew/F_mass - S_hat, self._P, self.P_tol, self.S_hat_tol, (S_hat,), checkiter=False, checkbounds=False, maxiter=self.maxiter, ) self._P = self._thermal_condition.P = P self._thermal_condition.T = T def set_PV(self, P, V, gas_conversion=None, liquid_conversion=None): self._setup(gas_conversion, liquid_conversion) self._thermal_condition.P = self._P = P if self._N == 0: raise RuntimeError('no chemicals present to perform VLE') if self._N == 1: return self._set_PV_chemical(P, V) # Setup bounderies thermal_condition = self._thermal_condition index = self._index vapor_mol = self._vapor_mol liquid_mol = self._liquid_mol if self._F_mol_heavy and V == 1.: V = 1. - 1e-3 if self._F_mol_light and V == 0.: V = 1e-3 if V == 1 and not self._F_mol_heavy: if gas_conversion: T_dew, dz, y, x_dew = self._dew_point.solve_Tx(self._z, P, gas_conversion) self._vapor_mol[self._index] = self._mol_vle + dz * self._F_mol_vle else: T_dew, x_dew = self._dew_point.solve_Tx(self._z, P) self._vapor_mol[self._index] = self._mol_vle self._liquid_mol[self._index] = 0 thermal_condition.T = T_dew elif V == 0 and not self._F_mol_light: if liquid_conversion: T_bubble, dz, y_bubble, x = self._bubble_point.solve_Ty(self._z, P, liquid_conversion) self._liquid_mol[self._index] = self._mol_vle + dz * self._F_mol_vle else: T_bubble, y_bubble = self._bubble_point.solve_Ty(self._z, P) self._liquid_mol[self._index] = self._mol_vle self._vapor_mol[self._index] = 0 thermal_condition.T = T_bubble else: if liquid_conversion: T_bubble, dz_bubble, y_bubble, _ = self._bubble_point.solve_Ty(self._z, P, liquid_conversion) else: T_bubble, y_bubble = self._bubble_point.solve_Ty(self._z, P) dz_bubble = None if gas_conversion: T_dew, dz_dew, y, x_dew = self._dew_point.solve_Tx(self._z, P, gas_conversion) else: T_dew, x_dew = self._dew_point.solve_Tx(self._z, P) dz_dew = None self._refresh_K(V, y_bubble, x_dew, dz_bubble, dz_dew) if self._F_mol_light: T_bubble = 0.1 * self._bubble_point.Tmin + 0.9 * T_bubble if self._F_mol_heavy: T_dew = 0.1 * self._bubble_point.Tmax + 0.9 * T_dew V_bubble = self._V_err_at_T(T_bubble, 0., gas_conversion, liquid_conversion) if V_bubble > V: F_mol = self._F_mol mol = self._mol_vle if liquid_conversion: mol = mol + self._dmol_vle F_mol = F_mol + self._dF_mol F_mol_vapor = F_mol * V v = y_bubble * F_mol_vapor mask = v > mol v[mask] = mol[mask] T = T_bubble else: V_dew = self._V_err_at_T(T_dew, 0., gas_conversion, liquid_conversion) if V_dew < V: F_mol = self._F_mol mol = self._mol_vle if gas_conversion: mol = mol + self._dmol_vle F_mol = F_mol + self._dF_mol l = x_dew * F_mol * (1. - V) mask = l > mol l[mask] = mol[mask] v = mol - l T = T_dew else: T = flx.IQ_interpolation( self._V_err_at_T, T_bubble, T_dew, V_bubble - V, V_dew - V, self._T, self.T_tol, self.V_tol, (V, gas_conversion, liquid_conversion), checkiter=False, checkbounds=False, maxiter=self.maxiter, ) v = self._v mol = self._mol_vle if gas_conversion or liquid_conversion: mol = mol + self._dmol_vle self._T = thermal_condition.T = T set_flows(vapor_mol, liquid_mol, index, v, mol) if liquid_conversion or gas_conversion: try: self._H_hat = ( self.mixture.xH(self._phase_data, T, P) + (self.chemicals.Hf * mol).sum(0) ) / self._F_mass except: pass else: try: self._H_hat = self.mixture.xH(self._phase_data, T, P) / self._F_mass except: pass def set_PS(self, P, S, gas_conversion=None, liquid_conversion=None): self._setup(gas_conversion, liquid_conversion) thermal_condition = self._thermal_condition thermal_condition.P = self._P = P if self._N == 0: thermal_condition.T = self.mixture.xsolve_T_at_SP( self._phase_data, S, thermal_condition.T, P ) return if self._N == 1: return self._set_PS_chemical(P, S) # Setup bounderies index = self._index mol = self._mol_vle vapor_mol = self._vapor_mol liquid_mol = self._liquid_mol # Check if subcooled liquid T_bubble, y_bubble = self._bubble_point.solve_Ty(self._z, P) if self._F_mol_light: T_bubble = 0.9 * T_bubble + 0.1 * self._bubble_point.Tmin vapor_mol[index] = 0 liquid_mol[index] = mol S_bubble = self.mixture.xS(self._phase_data, T_bubble, P) dS_bubble = S - S_bubble if dS_bubble <= 0: thermal_condition.T = self.mixture.xsolve_T_at_SP(self._phase_data, S, T_bubble, P) return # Check if super heated vapor T_dew, x_dew = self._dew_point.solve_Tx(self._z, P) if T_dew <= T_bubble: T_dew, T_bubble = T_bubble, T_dew T_dew += 0.5 T_bubble -= 0.5 if self._F_mol_heavy: T_dew = 0.9 * T_dew + 0.1 * self._dew_point.Tmax vapor_mol[index] = mol liquid_mol[index] = 0 S_dew = self.mixture.xS(self._phase_data, T_dew, P) dS_dew = S - S_dew if dS_dew >= 0: thermal_condition.T = self.mixture.xsolve_T_at_SP(self._phase_data, S, T_dew, P) return # Guess T, overall vapor fraction, and vapor flow rates V = dS_bubble/(S_dew - S_bubble) self._refresh_K(V, y_bubble, x_dew) F_mass = self._F_mass S_hat = S/F_mass S_hat_bubble = self._S_hat_err_at_T(T_bubble, 0.) if (S_hat_bubble:=self._S_hat_err_at_T(T_bubble, 0.)) > S_hat: T = T_bubble elif (S_hat_dew:=self._S_hat_err_at_T(T_dew, 0.)) < S_hat: T = T_dew else: T = flx.IQ_interpolation( self._S_hat_err_at_T, T_bubble, T_dew, S_hat_bubble - S_hat, S_hat_dew - S_hat, self._T, self.T_tol, self.S_hat_tol, (S_hat,), checkiter=False, checkbounds=False, maxiter=self.maxiter ) # Make sure energy balance is correct by vaporizing a fraction # of the liquid or condensing a fraction of the vapor self._T = thermal_condition.T = T mol_liq = liquid_mol.copy() mol_liq[:] = 0. mol_liq[index] = liquid_mol[index] mol_gas= vapor_mol.copy() mol_gas[:] = 0. mol_gas[index] = vapor_mol[index] S_gas = self.mixture.S('g', mol_gas, T, P) S_liq = self.mixture.S('l', mol_liq, T, P) S_current = self.mixture.xS(self._phase_data, T, P) if S_current > S: # Condense a fraction of the vapor # Energy balance: S = f * S_condense + S_current S_condense = self.mixture.S('l', mol_gas, T, P) - S_gas try: f = (S - S_current) / S_condense except: # Floating point errors f = 0 else: if f < 0.: f = 0. elif f > 0.: if f > 1.: f = 1. condensed = f * mol_gas[index] liquid_mol[index] += condensed vapor_mol[index] -= condensed else: f = 0. else: # Vaporize a fraction of the liquid # Energy balance: S = f * S_vaporise + S_current S_vaporise = self.mixture.S('g', mol_liq, T, P) - S_liq try: f = (S - S_current) / S_vaporise except: # Floating point errors f = 0 else: if f < 0.: f = 0. elif f > 0.: if f > 1.: f = 1. vaporised = f * mol_liq[index] vapor_mol[index] += vaporised liquid_mol[index] -= vaporised else: f = 0. if f == 0. or f == 1.: self._T = thermal_condition.T = self.mixture.xsolve_T_at_SP( self._phase_data, S, T, P ) self._S_hat = S_hat def set_PH(self, P, H, gas_conversion=None, liquid_conversion=None): self._setup(gas_conversion, liquid_conversion) thermal_condition = self._thermal_condition thermal_condition.P = self._P = P if self._N == 0: thermal_condition.T = self.mixture.xsolve_T_at_HP( self._phase_data, H, thermal_condition.T, P ) return if self._N == 1: return self._set_PH_chemical(P, H) # Setup bounderies index = self._index vapor_mol = self._vapor_mol liquid_mol = self._liquid_mol # Check if subcooled liquid if liquid_conversion: T_bubble, dz_bubble, y_bubble, x = self._bubble_point.solve_Ty(self._z, P, liquid_conversion) mol = self._mol_vle + dz_bubble * self._F_mol_vle else: T_bubble, y_bubble = self._bubble_point.solve_Ty(self._z, P) mol = self._mol_vle dz_bubble = None if self._F_mol_light: T_bubble = 0.9 * T_bubble + 0.1 * self._bubble_point.Tmin vapor_mol[index] = 0 liquid_mol[index] = mol H_bubble = self.mixture.xH(self._phase_data, T_bubble, P) if liquid_conversion: Hf = (self.chemicals.Hf * mol).sum() H_bubble += Hf dH_bubble = H - H_bubble if dH_bubble <= 0: if liquid_conversion: H -= Hf thermal_condition.T = self.mixture.xsolve_T_at_HP(self._phase_data, H, T_bubble, P) return # Check if super heated vapor if gas_conversion: T_dew, dz_dew, y, x_dew = self.dew_point.solve_Tx(self._z, P, gas_conversion) mol = self._mol_vle + dz_dew else: T_dew, x_dew = self._dew_point.solve_Tx(self._z, P) mol = self._mol_vle dz_dew = None if T_dew <= T_bubble: T_dew, T_bubble = T_bubble, T_dew T_dew += 0.5 T_bubble -= 0.5 if self._F_mol_heavy: T_dew = 0.9 * T_dew + 0.1 * self._dew_point.Tmax vapor_mol[index] = mol liquid_mol[index] = 0 H_dew = self.mixture.xH(self._phase_data, T_dew, P) if gas_conversion: Hf = (self.chemicals.Hf * mol).sum() H_dew += Hf dH_dew = H - H_dew if dH_dew >= 0: if gas_conversion: H -= Hf thermal_condition.T = self.mixture.xsolve_T_at_HP(self._phase_data, H, T_dew, P) return # Guess T, overall vapor fraction, and vapor flow rates V = abs(dH_bubble / (H_dew - H_bubble)) self._refresh_K(V, y_bubble, x_dew, dz_bubble, dz_dew) F_mass = self._F_mass H_hat = H/F_mass if (H_hat_bubble:=self._H_hat_err_at_T(T_bubble, 0., gas_conversion, liquid_conversion)) > H_hat: T = T_bubble elif (H_hat_dew:=self._H_hat_err_at_T(T_dew, 0., gas_conversion, liquid_conversion)) < H_hat: T = T_dew else: T = flx.IQ_interpolation( self._H_hat_err_at_T, T_bubble, T_dew, H_hat_bubble - H_hat, H_hat_dew - H_hat, self._T, self.T_tol, self.H_hat_tol, (H_hat, gas_conversion, liquid_conversion), checkiter=False, checkbounds=False, maxiter=self.maxiter ) if gas_conversion or liquid_conversion: H -= (self.chemicals.Hf * (self._mol_vle + self._dmol_vle)).sum() # Make sure energy balance is correct by vaporizing a fraction # of the liquid or condensing a fraction of the vapor self._T = thermal_condition.T = T mol_liq = liquid_mol.copy() mol_liq[:] = 0. mol_liq[index] = liquid_mol[index] mol_gas= vapor_mol.copy() mol_gas[:] = 0. mol_gas[index] = vapor_mol[index] H_gas = self.mixture.H('g', mol_gas, T, P) H_liq = self.mixture.H('l', mol_liq, T, P) H_current = self.mixture.xH(self._phase_data, T, P) if H_current > H: # Condense a fraction of the vapor # Energy balance: H = f * H_condense + H_current H_condense = self.mixture.H('l', mol_gas, T, P) - H_gas try: f = (H - H_current) / H_condense except: # Floating point errors f = 0 else: if f < 0.: f = 0. elif f > 0.: if f > 1.: f = 1. condensed = f * mol_gas[index] liquid_mol[index] += condensed vapor_mol[index] -= condensed else: f = 0 else: # Vaporize a fraction of the liquid # Energy balance: H = f * H_vaporise + H_current H_vaporise = self.mixture.H('g', mol_liq, T, P) - H_liq try: f = (H - H_current) / H_vaporise except: # Floating point errors f = 0 else: if f < 0.: f = 0. elif f > 0.: if f > 1.: f = 1. vaporised = f * mol_liq[index] vapor_mol[index] += vaporised liquid_mol[index] -= vaporised else: f = 0 if f == 0. or f == 1.: self._T = thermal_condition.T = self.mixture.xsolve_T_at_HP( self._phase_data, H, T, P ) self._H_hat = H_hat def _refresh_K(self, V, y_bubble, x_dew, dz_bubble=None, dz_dew=None): # TODO: use reaction data here for better estimate F_mol_vle = self._F_mol_vle z = self._z_norm L = (1 - V) if dz_bubble is not None: F_mol_vle = F_mol_vle - dz_bubble * L * F_mol_vle if dz_dew is not None: F_mol_vle = F_mol_vle - dz_dew * V * F_mol_vle v_bubble = (V * z + L * y_bubble) * V * F_mol_vle v_dew = (z - L * (L * z + V * x_dew)) * F_mol_vle v = L * v_bubble + V * v_dew l = self._mol_vle - v y = v / v.sum() l_net = l.sum() x = l / l_net if l_net > 0 else l * 0 x[x < 1e-32] = 1e-32 self._K = y / x self._V = V def _H_hat_err_at_T(self, T, H_hat, gas_conversion, liquid_conversion): v = self._solve_v(T, self._P, gas_conversion, liquid_conversion) if gas_conversion or liquid_conversion: mol_vle = self._mol_vle + self._dmol_vle set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle) H = self.mixture.xH(self._phase_data, T, self._P) + (self.chemicals.Hf * mol_vle).sum() else: mol_vle = self._mol_vle set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle) H = self.mixture.xH(self._phase_data, T, self._P) self._H_hat = H / self._F_mass return self._H_hat - H_hat def _H_hat_err_at_P(self, P, H_hat, gas_conversion=None, liquid_conversion=None): v = self._solve_v(self._T, P, gas_conversion, liquid_conversion) if gas_conversion or liquid_conversion: mol_vle = self._mol_vle + self._dmol_vle set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle) H = self.mixture.xH(self._phase_data, self._T, P) + (self.chemicals.Hf * mol_vle).sum() else: mol_vle = self._mol_vle set_flows(self._vapor_mol, self._liquid_mol, self._index, v, mol_vle) H = self.mixture.xH(self._phase_data, self._T, P) self._H_hat = H / self._F_mass return self._H_hat - H_hat def _S_hat_err_at_T(self, T, S_hat): set_flows(self._vapor_mol, self._liquid_mol, self._index, self._solve_v(T, self._P), self._mol_vle) self._S_hat = self.mixture.xS(self._phase_data, T, self._P)/self._F_mass return self._S_hat - S_hat def _S_hat_err_at_P(self, P, S_hat): set_flows(self._vapor_mol, self._liquid_mol, self._index, self._solve_v(self._T, P), self._mol_vle) self._S_hat = self.mixture.xS(self._phase_data, self._T, P)/self._F_mass return self._S_hat - S_hat def _V_err_at_P(self, P, V, gas_conversion, liquid_conversion): v = self._solve_v(self._T , P, gas_conversion, liquid_conversion).sum() if gas_conversion or liquid_conversion: F_mol_vle = self._F_mol_vle + self._dF_mol else: F_mol_vle = self._F_mol_vle return v / F_mol_vle - V def _V_err_at_T(self, T, V, gas_conversion, liquid_conversion): v = self._solve_v(T, self._P, gas_conversion, liquid_conversion).sum() if gas_conversion or liquid_conversion: F_mol_vle = self._F_mol_vle + self._dF_mol else: F_mol_vle = self._F_mol_vle return v / F_mol_vle - V def _solve_v(self, T, P, gas_conversion=None, liquid_conversion=None): """Solve for vapor mol""" method = self.method if method == 'shgo': if gas_conversion or liquid_conversion: raise RuntimeError('shgo is not a valid method (yet) when reactions are present') gamma = self._gamma phi = self._phi Psats = np.array([i(T) for i in self._bubble_point.Psats]) pcf = self._pcf(T, P, Psats) F_mol_vle = self._F_mol_vle mol_vle = self._mol_vle z = mol_vle / F_mol_vle self._v = v = F_mol_vle * solve_vle_vapor_mol_shgo( z, T, gamma.f, gamma.args, P, pcf * Psats, phi.f, phi.args, dict(f_tol=self.y_tol, minimizer_kwargs=dict(f_tol=self.y_tol)), ) self._z_last = z elif method == 'fixed-point': Psats = np.array([i(T) for i in self._bubble_point.Psats]) pcf_Psats_over_P = self._pcf(T, P, Psats) * Psats / P self._T = T self._v = v = self._solve_v_fixed_point(pcf_Psats_over_P, T, P, gas_conversion, liquid_conversion) if gas_conversion or liquid_conversion: mol_vle = self._mol_vle + self._dmol_vle else: mol_vle = self._mol_vle mask = v > mol_vle v[mask] = mol_vle[mask] v[v < 0.] = 0. else: raise RuntimeError(f"invalid method '{method}'") return v def _solve_v_fixed_point(self, pcf_Psat_over_P, T, P, gas_conversion, liquid_conversion): gamma = self._gamma z = self._z K = self._K # pcf_Psat_over_P n = z.size if n > 2 or self._z_light or self._z_heavy: f = xVlogK_iter args = (pcf_Psat_over_P, T, P, z, self._z_light, self._z_heavy, gamma.f, gamma.args, self._phi, n, gas_conversion, liquid_conversion) else: f = xVlogK_iter_2n args = (pcf_Psat_over_P, T, P, z, gamma.f, gamma.args, self._phi, n, gas_conversion, liquid_conversion) xVlogK = np.zeros(2 * n + 1) xVlogK[n] = V = self._V K[K < 1e-16] = 1e-16 xVlogK[:n] = z/(1. + V * (K - 1.)) xVlogK[n+1:] = np.log(K) xVlogK = flx.aitken( f, xVlogK, self.K_tol, args, checkiter=False, checkconvergence=False, convergenceiter=5, maxiter=self.maxiter ) self._V = V = xVlogK[n] self._K = K = np.exp(xVlogK[n+1:]) x = xVlogK[:n] x, y = xy(x, K) self._z_last = z if gas_conversion or liquid_conversion: dmol_vle = 0 if gas_conversion: dmol_vle += gas_conversion(material=y * V, T=T, P=P, phase='g') if liquid_conversion: dmol_vle += liquid_conversion(material=x * (1 - V), T=T, P=P, phase='l') dmol_vle *= self._F_mol self._dmol_vle = dmol_vle self._dF_mol = dmol_vle.sum() v = (self._F_mol - self._dF_mol) * V * x * K else: v = self._F_mol * V * x * K return v def _setup(self, gas_conversion=None, liquid_conversion=None): imol = self._imol self._phase_data = tuple(imol) self._liquid_mol = liquid_mol = imol['l'] self._vapor_mol = vapor_mol = imol['g'] mol = liquid_mol + vapor_mol if not mol.any(): raise NoEquilibrium('no chemicals to perform equilibrium') nonzero = set(mol.nonzero_keys()) if gas_conversion: nonzero.update( gas_conversion.reaction.stoichiometry.nonzero_keys() ) if liquid_conversion: nonzero.update( liquid_conversion.reaction.stoichiometry.nonzero_keys() ) chemicals = self.chemicals if self._nonzero == nonzero: index = self._index eq_chems = chemicals.tuple eq_chems = [eq_chems[i] for i in index] reset = False else: # Set up indices for both equilibrium and non-equilibrium species index = chemicals.get_vle_indices(nonzero) eq_chems = chemicals.tuple eq_chems = [eq_chems[i] for i in index] reset = True self._nonzero = nonzero self._index = index if gas_conversion and [i.ID for i in eq_chems] != [i.ID for i in gas_conversion.reaction.chemicals]: gas_conversion.set_chemicals(eq_chems) if liquid_conversion and [i.ID for i in eq_chems] != [i.ID for i in liquid_conversion.reaction.chemicals]: liquid_conversion.set_chemicals(eq_chems) # Get overall composition self._F_mass = (chemicals.MW * mol).sum() self._mol_vle = mol_vle = mol[index] # Set light and heavy keys LNK_index = chemicals._light_indices HNK_index = chemicals._heavy_indices vapor_mol[HNK_index] = 0 vapor_mol[LNK_index] = light_mol = mol[LNK_index] liquid_mol[LNK_index] = 0 liquid_mol[HNK_index] = heavy_mol = mol[HNK_index] if not mol_vle.any(): raise NoEquilibrium('no chemicals to perform equilibrium') self._F_mol_light = F_mol_light = light_mol.sum() self._F_mol_heavy = F_mol_heavy = (heavy_mol * chemicals._heavy_solutes).sum() self._F_mol_vle = F_mol_vle = mol_vle.sum() self._F_mol = F_mol = F_mol_vle + F_mol_light + F_mol_heavy self._z = mol_vle / F_mol self._z_light = z_light = F_mol_light / F_mol self._z_heavy = z_heavy = F_mol_heavy / F_mol self._z_norm = mol_vle / F_mol_vle N = len(index) if N: N += z_light > 0. N += z_heavy > 0. self._N = N if reset: if N == 0: self._phi = self._gamma = self._pcf = self._dew_point = self._bubble_point = None elif N == 1: self._chemical, = eq_chems else: # Set equilibrium objects thermo = self._thermo self._bubble_point = bp = BubblePoint(eq_chems, thermo) self._dew_point = DewPoint(eq_chems, thermo) self._pcf = bp.pcf self._gamma = bp.gamma self._phi = bp.phi
class VLECache(Cache): load = VLE del Cache, Equilibrium @njit(cache=True) def liquid_fugacity(mol_L, T, pcf_Psats, f_gamma, gamma_args): total_mol_L = mol_L.sum() if total_mol_L: x = mol_L / total_mol_L fugacity = x * f_gamma(x, T, *gamma_args) * pcf_Psats else: fugacity = np.ones_like(mol_L) return fugacity @njit(cache=True) def vapor_fugacity(mol_v, T, P, f_phi, phi_args): total_mol_v = mol_v.sum() if total_mol_v: y = mol_v / total_mol_v fugacity = y * P * f_phi(y, T, P, *phi_args) else: fugacity = np.ones_like(mol_v) return fugacity @njit(cache=True) def gibbs_free_energy(mol, fugacity): fugacity[fugacity <= 0] = 1 g_mix = (mol * np.log(fugacity)).sum() return g_mix @njit(cache=True) def vle_objective_function(mol_v, mol, T, f_gamma, gamma_args, P, pcf_Psats, f_phi, phi_args): mol_l = mol - mol_v g_mix_l = gibbs_free_energy(mol_l, liquid_fugacity(mol_l, T, pcf_Psats, f_gamma, gamma_args)) g_mix_g = gibbs_free_energy(mol_v, vapor_fugacity(mol_v, T, P, f_phi, phi_args)) g_mix = g_mix_l + g_mix_g return g_mix def solve_vle_vapor_mol_shgo( mol, T, f_gamma, gamma_args, P, pcf_Psats, f_phi, phi_args, shgo_options, ): args = (mol, T, f_gamma, gamma_args, P, pcf_Psats, f_phi, phi_args) bounds = np.zeros([mol.size, 2]) bounds[:, 1] = mol result = shgo(vle_objective_function, bounds, args, options=shgo_options) return result.x