Source code for idaes.models.properties.general_helmholtz.helmholtz_functions

#################################################################################
# The Institute for the Design of Advanced Energy Systems Integrated Platform
# Framework (IDAES IP) was produced under the DOE Institute for the
# Design of Advanced Energy Systems (IDAES).
#
# Copyright (c) 2018-2024 by the software owners: The Regents of the
# University of California, through Lawrence Berkeley National Laboratory,
# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
# University, West Virginia University Research Corporation, et al.
# All rights reserved.  Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
"""Generic Helmholtz EOS Functions and Parameters
"""

__author__ = "John Eslick"

import enum
import ctypes
import os

from matplotlib import pyplot as plt

import pyomo.environ as pyo
from pyomo.common.fileutils import find_library
from pyomo.common.config import ConfigValue, In

# importing numpy from pyomo forces proper handling of numpy types in Pyomo
# this is a workaround for a lingering issue after Pyomo/pyomo@cfa6ff49
# (see IDAES/idaes-pse#1348) until this is addressed in Pyomo
from pyomo.common.dependencies import numpy as np
from idaes.core.util.exceptions import ConfigurationError
from idaes.core import declare_process_block_class
from idaes.core import (
    PhysicalParameterBlock,
    LiquidPhase,
    VaporPhase,
    Phase,
    Component,
)
from idaes.models.properties.general_helmholtz.components import (
    viscosity_available,
    thermal_conductivity_available,
    surface_tension_available,
    component_registered,
)
from idaes.models.properties.general_helmholtz.components.parameters import (
    get_parameter_path,
    auto_register,
)
import idaes.logger as idaeslog
from idaes.models.properties.general_helmholtz.helmholtz_functions_map import (
    external_function_map as _external_function_map,
)


_log = idaeslog.getLogger(__name__)

_data_dir = get_parameter_path()
_data_dir = os.path.join(_data_dir, "")
auto_register()

# General Helmholtz functions return variables for all phases,
# but single phase properties do not need all of these.
# pylint: disable=W0612

try:
    _flib = find_library("general_helmholtz_external")
    ctypes.cdll.LoadLibrary(_flib)
except Exception:  # pylint: disable=W0703
    _flib = None


def helmholtz_available():
    """Returns True if the shared library is installed and loads properly
    otherwise returns False
    """
    if _flib is None:
        return False
    if not os.path.exists(_data_dir):
        _log.error(f"The Helmholtz EoS data directory {_data_dir} does not exist.")
        return False
    return True


helmholtz_data_dir = _data_dir


[docs] class StateVars(enum.Enum): """ Enum, state variable set options. * PH: Pressure and enthalpy * PS: Pressure and entropy * PU: Pressure and internal energy * TPX: Temperature, pressure, and quality """ PH = 1 # Pressure, Enthalpy PS = 2 # Pressure, Entropy PU = 3 # Pressure, Internal Energy TPX = 4 # Temperature, Pressure, Quality
[docs] class PhaseType(enum.Enum): """ Enum, possible phases and presentation. * MIX: Two phase presented and a single phase to framework * LG: Two phases * L: Liquid only * G: Vapor only """ MIX = 1 # Looks like a single phase called mixed with a vapor fraction LG = 2 # Looks like two phases vapor and liquid L = 3 # Assume only liquid is present G = 4 # Assume only vapor is present
[docs] class AmountBasis(enum.Enum): """ Enum, mass or mole basis * MOLE: Amount is measured in moles * MASS: Amount is measured in mass """ MOLE = 1 MASS = 2
def add_helmholtz_external_functions(blk, names=None): """Add a Helmholtz EoS function to a Pyomo Block. Args: blk: block to add function to names: if None, add all functions, if a string add the single function, otherwise a list of function names. Returns: None """ if names is None: names = _external_function_map.keys() if isinstance(names, str): names = [names] for name in names: if hasattr(blk, name): continue fdict = _external_function_map[name] setattr( blk, name, pyo.ExternalFunction( library=_flib, function=fdict["fname"], units=fdict["units"], arg_units=fdict["arg_units"], doc=fdict.get("doc", None), ), )
[docs] class HelmholtzThermoExpressions(object): """Class to write thermodynamic property expressions. Take one of these possible sets of state variables: {h, p}, {u, p}, {s, p}, {s, T}, {T, x}, {P, x}, or {T, P, x}, and return an expression for a thermo property. This works by converting the given state variables to temperature, density, and vapor fraction expressions then using those to write an expression for requested property. This writes expressions in a way that looks like a thermodynamic property function. Args: blk (Block): block to attach the external functions to parameters (HelmholtzParameterBlock): property parameter block amount_basis (AmountBasis|None): If none get the amount basis from the parameter block, otherwise use this amount basis for inputs. Returns: HelmholtzThermoExpressions """ def __init__(self, blk, parameters, amount_basis=None): """Create a new thermodynamic property expression writer class. Args: blk (Block): block to attach the external functions to parameters (HelmholtzParameterBlock): property parameter block amount_basis (AmountBasis|None): If none get the amount basis from the parameter block, otherwise use this amount basis for inputs. Returns: HelmholtzThermoExpressions """ if not helmholtz_available(): raise RuntimeError("Helmholtz EoS external functions not available") if amount_basis is None: amount_basis = parameters.config.amount_basis self.param = parameters self.blk = blk self.amount_basis = amount_basis
[docs] def add_funcs(self, names=None): """Add external functions for the block expressions will be written from. Args: name (str): function name """ add_helmholtz_external_functions(self.blk, names=names)
@staticmethod def _validate_args(kwargs): """Make sure the provided arguments in kwargs is valid. For now the supported options for state variables are: (h, p), (s, p), (u, p), (T, p, x), and for single phase only (T, p) For sat properties, (T, x) and (p, x) are also valid, Psat or Tsat will be added automatically as the missing sate variable. """ new_kwargs = {a: kwargs[a] for a in kwargs if kwargs[a] is not None} kwset = set(new_kwargs.keys()) if len(new_kwargs) == 3: if {"T", "p", "x"} == set(new_kwargs): return new_kwargs, kwset if len(new_kwargs) != 2: raise RuntimeError( f"Helmholtz EoS expression writer: state variable set {kwset} is not supported." ) if kwset in [ {"T", "p"}, {"h", "p"}, {"s", "p"}, {"u", "p"}, {"T", "x"}, {"p", "x"}, ]: return new_kwargs, kwset raise RuntimeError( f"Helmholtz EoS expression writer: state variable set {kwset} is not supported." ) def _state_vars(self, **kwargs): """ This function takes the state variable args, and if they are in one of the sets (h, p), (s, p) or (u, p), it returns (enum for state variable set, block with external function objects, the component string, the first state variable with units for external function, and the second state variable with units for the external functions). If the state variable set is not one of the above, this function returns None for all but the block and component. """ result_basis = kwargs.pop("result_basis", self.amount_basis) convert_args = kwargs.pop("convert_args", True) kwargs, kwset = self._validate_args(kwargs) c = self.param.pure_component # string for chemical component blk = self.blk # block with external functions if "p" in kwargs: if convert_args: p = kwargs["p"] * self.param.uc["Pa to kPa"] else: p = kwargs["p"] if "x" in kwargs: x = kwargs["x"] else: x = None if "T" in kwargs: T = kwargs["T"] if {"h", "p"} == kwset: if convert_args: if self.amount_basis == AmountBasis.MOLE: h = kwargs["h"] * self.param.uc["J/mol to kJ/kg"] else: h = kwargs["h"] * self.param.uc["J/kg to kJ/kg"] else: h = kwargs["h"] return StateVars.PH, result_basis, blk, c, h, p, None if {"s", "p"} == kwset: if convert_args: if self.amount_basis == AmountBasis.MOLE: s = kwargs["s"] * self.param.uc["J/mol/K to kJ/kg/K"] else: s = kwargs["s"] * self.param.uc["J/kg/K to kJ/kg/K"] else: s = kwargs["s"] return StateVars.PS, result_basis, blk, c, s, p, None if {"u", "p"} == kwset: if convert_args: if self.amount_basis == AmountBasis.MOLE: u = kwargs["u"] * self.param.uc["J/mol to kJ/kg"] else: u = kwargs["u"] * self.param.uc["J/kg to kJ/kg"] else: u = kwargs["u"] return StateVars.PU, result_basis, blk, c, u, p, None if {"T", "x"} == kwset: self.add_funcs(names=["p_sat_t_func"]) p = blk.p_sat_t_func(c, T, _data_dir) return StateVars.TPX, result_basis, blk, c, T, p, x if {"p", "x"} == kwset: self.add_funcs(names=["t_sat_func"]) T = blk.t_sat_func(c, p, _data_dir) return StateVars.TPX, result_basis, blk, c, T, p, x if kwset in [{"T", "p"}, {"T", "p", "x"}]: return StateVars.TPX, result_basis, blk, c, T, p, x def _generic_prop( self, hp_func, up_func, sp_func, tp_liq_func, tp_vap_func, mass_uc, mole_uc, **kwargs, ): """mixed phase property""" sv, result_basis, blk, c, state1, p, x = self._state_vars(**kwargs) if sv == StateVars.PH: self.add_funcs(names=[hp_func]) prop = getattr(blk, hp_func)(c, state1, p, _data_dir) elif sv == StateVars.PU: self.add_funcs(names=[up_func]) prop = getattr(blk, up_func)(c, state1, p, _data_dir) elif sv == StateVars.PS: self.add_funcs(names=[sp_func]) prop = getattr(blk, sp_func)(c, state1, p, _data_dir) else: if x is None: raise RuntimeError("Cannot calculate vapor fraction from T and p") self.add_funcs(names=[tp_liq_func]) self.add_funcs(names=[tp_vap_func]) prop = ( getattr(blk, tp_liq_func)(c, state1, p, _data_dir) * (1 - x) + getattr(blk, tp_vap_func)(c, state1, p, _data_dir) * x ) if result_basis == AmountBasis.MOLE: return prop * mole_uc return prop * mass_uc def _generic_prop_phase( self, hp_func, up_func, sp_func, tp_func, mass_uc, mole_uc, **kwargs, ): """single phase property""" sv, result_basis, blk, c, state1, p, x = self._state_vars(**kwargs) if sv == StateVars.PH: self.add_funcs(names=[hp_func]) prop = getattr(blk, hp_func)(c, state1, p, _data_dir) elif sv == StateVars.PU: self.add_funcs(names=[up_func]) prop = getattr(blk, up_func)(c, state1, p, _data_dir) elif sv == StateVars.PS: self.add_funcs(names=[sp_func]) prop = getattr(blk, sp_func)(c, state1, p, _data_dir) else: self.add_funcs(names=[tp_func]) prop = getattr(blk, tp_func)(c, state1, p, _data_dir) if result_basis == AmountBasis.MOLE: return prop * mole_uc return prop * mass_uc
[docs] def p(self, **kwargs): """Pressure""" # The only way we'd be calculating pressure is when we have T, x. In # that case, it has been calculated already, so just return pressure. sv, result_basis, blk, c, u, p, x = self._state_vars(**kwargs) return p * self.param.uc["kPa to Pa"]
[docs] def T(self, **kwargs): """Temperature""" sv, results_basis, blk, c, state1, p, x = self._state_vars(**kwargs) if sv == StateVars.PH: self.add_funcs(names=["t_hp_func"]) t = blk.t_hp_func(c, state1, p, _data_dir) elif sv == StateVars.PU: self.add_funcs(names=["t_up_func"]) t = blk.t_up_func(c, state1, p, _data_dir) elif sv == StateVars.PS: self.add_funcs(names=["t_sp_func"]) t = blk.t_sp_func(c, state1, p, _data_dir) else: t = state1 return t
[docs] def tau(self, **kwargs): """Critical Temperature (K)/Temperature (K)""" sv, result_basis, blk, c, state1, p, x = self._state_vars(**kwargs) if sv == StateVars.PH: self.add_funcs(names=["tau_hp_func"]) tau = blk.tau_hp_func(c, state1, p, _data_dir) elif sv == StateVars.PU: self.add_funcs(names=["tau_up_func"]) tau = blk.tau_up_func(c, state1, p, _data_dir) elif sv == StateVars.PS: self.add_funcs(names=["tau_sp_func"]) tau = blk.tau_sp_func(c, state1, p, _data_dir) else: tau = self.param.temperature_star / state1 return tau
[docs] def x(self, **kwargs): """Vapor fraction""" sv, result_basis, blk, c, state1, p, x = self._state_vars(**kwargs) if sv == StateVars.PH: self.add_funcs(names=["vf_hp_func"]) x = blk.vf_hp_func(c, state1, p, _data_dir) elif sv == StateVars.PU: self.add_funcs(names=["vf_up_func"]) x = blk.vf_up_func(c, state1, p, _data_dir) elif sv == StateVars.PS: self.add_funcs(names=["vf_up_func"]) x = blk.vf_up_func(c, state1, p, _data_dir) elif sv == StateVars.TPX: if x is None: raise RuntimeError("There is no nice way to calculate x from T and P") return x
[docs] def s(self, **kwargs): """Mixed phase entropy""" sv, result_basis, blk, c, state1, p, x = self._state_vars(**kwargs) if sv == StateVars.PH: self.add_funcs(names=["s_hp_func"]) s = blk.s_hp_func(c, state1, p, _data_dir) elif sv == StateVars.PU: self.add_funcs(names=["s_up_func"]) s = blk.s_up_func(c, state1, p, _data_dir) elif sv == StateVars.PS: s = state1 elif sv == StateVars.TPX: if x is None: raise RuntimeError("Cannot calculate vapor fraction from T and p") self.add_funcs(names=["s_vap_tp_func"]) self.add_funcs(names=["s_liq_tp_func"]) s = ( blk.s_liq_tp_func(c, state1, p, _data_dir) * (1 - x) + blk.s_vap_tp_func(c, state1, p, _data_dir) * x ) if result_basis == AmountBasis.MOLE: return s * self.param.uc["kJ/kg/K to J/mol/K"] return s * self.param.uc["kJ/kg/K to J/kg/K"]
[docs] def s_liq(self, **kwargs): """Liquid phase entropy""" return self._generic_prop_phase( hp_func="s_liq_hp_func", up_func="s_liq_up_func", sp_func="s_liq_sp_func", tp_func="s_liq_tp_func", mass_uc=self.param.uc["kJ/kg/K to J/kg/K"], mole_uc=self.param.uc["kJ/kg/K to J/mol/K"], **kwargs, )
[docs] def s_vap(self, **kwargs): """Vapor phase entropy""" return self._generic_prop_phase( hp_func="s_vap_hp_func", up_func="s_vap_up_func", sp_func="s_vap_sp_func", tp_func="s_vap_tp_func", mass_uc=self.param.uc["kJ/kg/K to J/kg/K"], mole_uc=self.param.uc["kJ/kg/K to J/mol/K"], **kwargs, )
[docs] def h(self, **kwargs): """Mixed phase enthalpy""" sv, result_basis, blk, c, state1, p, x = self._state_vars(**kwargs) if sv == StateVars.PH: h = state1 elif sv == StateVars.PU: self.add_funcs(names=["h_up_func"]) h = blk.h_up_func(c, state1, p, _data_dir) elif sv == StateVars.PS: self.add_funcs(names=["h_sp_func"]) h = blk.h_sp_func(c, state1, p, _data_dir) elif sv == StateVars.TPX: if x is None: raise RuntimeError("Cannot calculate vapor fraction from T and p") self.add_funcs(names=["h_vap_tp_func"]) self.add_funcs(names=["h_liq_tp_func"]) h = ( blk.h_liq_tp_func(c, state1, p, _data_dir) * (1 - x) + blk.h_vap_tp_func(c, state1, p, _data_dir) * x ) if result_basis == AmountBasis.MOLE: return h * self.param.uc["kJ/kg to J/mol"] return h * self.param.uc["kJ/kg to J/kg"]
[docs] def h_liq(self, **kwargs): """Liquid phase enthalpy""" return self._generic_prop_phase( hp_func="h_liq_hp_func", up_func="h_liq_up_func", sp_func="h_liq_sp_func", tp_func="h_liq_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def h_vap(self, **kwargs): """Vapor phase enthalpy""" return self._generic_prop_phase( hp_func="h_vap_hp_func", up_func="h_vap_up_func", sp_func="h_vap_sp_func", tp_func="h_vap_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def u(self, **kwargs): """Mixed phase internal energy""" sv, result_basis, blk, c, state1, p, x = self._state_vars(**kwargs) if sv == StateVars.PH: self.add_funcs(names=["u_hp_func"]) u = blk.u_hp_func(c, state1, p, _data_dir) elif sv == StateVars.PU: u = state1 elif sv == StateVars.PS: self.add_funcs(names=["u_sp_func"]) u = blk.u_sp_func(c, state1, p, _data_dir) elif sv == StateVars.TPX: if x is None: raise RuntimeError("Cannot calculate vapor fraction from T and p") self.add_funcs(names=["u_vap_tp_func"]) self.add_funcs(names=["u_liq_tp_func"]) u = ( blk.u_liq_tp_func(c, state1, p, _data_dir) * (1 - x) + blk.u_vap_tp_func(c, state1, p, _data_dir) * x ) if result_basis == AmountBasis.MOLE: return u * self.param.uc["kJ/kg to J/mol"] return u * self.param.uc["kJ/kg to J/kg"]
[docs] def u_liq(self, **kwargs): """Liquid phase internal energy""" return self._generic_prop_phase( hp_func="u_liq_hp_func", up_func="u_liq_up_func", sp_func="u_liq_sp_func", tp_func="u_liq_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def u_vap(self, **kwargs): """Vapor phase internal energy""" return self._generic_prop_phase( hp_func="u_vap_hp_func", up_func="u_vap_up_func", sp_func="u_vap_sp_func", tp_func="u_vap_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def g(self, **kwargs): """Mixed phase Gibbs free energy""" return self._generic_prop( hp_func="g_hp_func", up_func="g_up_func", sp_func="g_sp_func", tp_liq_func="g_liq_tp_func", tp_vap_func="g_vap_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def g_liq(self, **kwargs): """Liquid phase Gibbs free energy""" return self._generic_prop_phase( hp_func="g_liq_hp_func", up_func="g_liq_up_func", sp_func="g_liq_sp_func", tp_func="g_liq_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def g_vap(self, **kwargs): """Vapor phase Gibbs free energy""" return self._generic_prop_phase( hp_func="g_vap_hp_func", up_func="g_vap_up_func", sp_func="g_vap_sp_func", tp_func="g_vap_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def f(self, **kwargs): """Mixed phase Helmholtz free energy""" return self._generic_prop( hp_func="f_hp_func", up_func="f_up_func", sp_func="f_sp_func", tp_liq_func="f_liq_tp_func", tp_vap_func="f_vap_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def f_liq(self, **kwargs): """Liquid phase Helmholtz free energy""" return self._generic_prop_phase( hp_func="f_liq_hp_func", up_func="f_liq_up_func", sp_func="f_liq_sp_func", tp_func="f_liq_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def f_vap(self, **kwargs): """Vapor phase Helmholtz free energy""" return self._generic_prop_phase( hp_func="f_vap_hp_func", up_func="f_vap_up_func", sp_func="f_vap_sp_func", tp_func="f_vap_tp_func", mass_uc=self.param.uc["kJ/kg to J/kg"], mole_uc=self.param.uc["kJ/kg to J/mol"], **kwargs, )
[docs] def v(self, **kwargs): """Mixed phase volume""" return self._generic_prop( hp_func="v_hp_func", up_func="v_up_func", sp_func="v_sp_func", tp_liq_func="v_liq_tp_func", tp_vap_func="v_vap_tp_func", mass_uc=1, mole_uc=self.param.uc["m3/kg to m3/mol"], **kwargs, )
[docs] def v_liq(self, **kwargs): """Liquid phase volume""" return self._generic_prop_phase( hp_func="v_liq_hp_func", up_func="v_liq_up_func", sp_func="v_liq_sp_func", tp_func="v_liq_tp_func", mass_uc=1, mole_uc=self.param.uc["m3/kg to m3/mol"], **kwargs, )
[docs] def v_vap(self, **kwargs): """Vapor phase volume""" return self._generic_prop_phase( hp_func="v_vap_hp_func", up_func="v_vap_up_func", sp_func="v_vap_sp_func", tp_func="v_vap_tp_func", mass_uc=1, mole_uc=self.param.uc["m3/kg to m3/mol"], **kwargs, )
[docs] def rho(self, **kwargs): """Mixed phase density""" return 1 / self.v(**kwargs)
[docs] def rho_liq(self, **kwargs): """Liquid density""" return 1 / self.v_liq(**kwargs)
[docs] def rho_vap(self, **kwargs): """Vapor Density""" return 1 / self.v_vap(**kwargs)
[docs] def cp(self, **kwargs): """Mixed phase isobaric heat capacity""" return self._generic_prop( hp_func="cp_hp_func", up_func="cp_up_func", sp_func="cp_sp_func", tp_liq_func="cp_liq_tp_func", tp_vap_func="cp_vap_tp_func", mass_uc=self.param.uc["kJ/kg/K to J/kg/K"], mole_uc=self.param.uc["kJ/kg/K to J/mol/K"], **kwargs, )
[docs] def cp_liq(self, **kwargs): """Liquid phase isobaric heat capacity""" return self._generic_prop_phase( hp_func="cp_liq_hp_func", up_func="cp_liq_up_func", sp_func="cp_liq_sp_func", tp_func="cp_liq_tp_func", mass_uc=self.param.uc["kJ/kg/K to J/kg/K"], mole_uc=self.param.uc["kJ/kg/K to J/mol/K"], **kwargs, )
[docs] def cp_vap(self, **kwargs): """Vapor phase isobaric heat capacity""" return self._generic_prop_phase( hp_func="cp_vap_hp_func", up_func="cp_vap_up_func", sp_func="cp_vap_sp_func", tp_func="cp_vap_tp_func", mass_uc=self.param.uc["kJ/kg/K to J/kg/K"], mole_uc=self.param.uc["kJ/kg/K to J/mol/K"], **kwargs, )
[docs] def cv(self, **kwargs): """Mixed phase isochoric heat capacity""" return self._generic_prop( hp_func="cv_hp_func", up_func="cv_up_func", sp_func="cv_sp_func", tp_liq_func="cv_liq_tp_func", tp_vap_func="cv_vap_tp_func", mass_uc=self.param.uc["kJ/kg/K to J/kg/K"], mole_uc=self.param.uc["kJ/kg/K to J/mol/K"], **kwargs, )
[docs] def cv_liq(self, **kwargs): """Liquid phase isochoric heat capacity""" return self._generic_prop_phase( hp_func="cv_liq_hp_func", up_func="cv_liq_up_func", sp_func="cv_liq_sp_func", tp_func="cv_liq_tp_func", mass_uc=self.param.uc["kJ/kg/K to J/kg/K"], mole_uc=self.param.uc["kJ/kg/K to J/mol/K"], **kwargs, )
[docs] def cv_vap(self, **kwargs): """Vapor phase isochoric heat capacity""" return self._generic_prop_phase( hp_func="cv_vap_hp_func", up_func="cv_vap_up_func", sp_func="cv_vap_sp_func", tp_func="cv_vap_tp_func", mass_uc=self.param.uc["kJ/kg/K to J/kg/K"], mole_uc=self.param.uc["kJ/kg/K to J/mol/K"], **kwargs, )
[docs] def w(self, **kwargs): """Mixed phase speed of sound (value for two-phase is not meaningful) use the liquid or vapor version if two phases are expected. """ return self._generic_prop( hp_func="w_hp_func", up_func="w_up_func", sp_func="w_sp_func", tp_liq_func="w_liq_tp_func", tp_vap_func="w_vap_tp_func", mass_uc=1, mole_uc=1, **kwargs, )
[docs] def w_liq(self, **kwargs): """Liquid phase speed of sound""" return self._generic_prop_phase( hp_func="w_liq_hp_func", up_func="w_liq_up_func", sp_func="w_liq_sp_func", tp_func="w_liq_tp_func", mass_uc=1, mole_uc=1, **kwargs, )
[docs] def w_vap(self, **kwargs): """Vapor phase speed of sound""" return self._generic_prop_phase( hp_func="w_vap_hp_func", up_func="w_vap_up_func", sp_func="w_vap_sp_func", tp_func="w_vap_tp_func", mass_uc=1, mole_uc=1, **kwargs, )
[docs] def viscosity(self, **kwargs): """Mixed phase viscosity (value for two-phase is not meaningful) use the liquid or vapor version if two phases are expected. """ if not viscosity_available(self.param.pure_component): raise RuntimeError( f"Viscosity not available for {self.param.pure_component}" ) return self._generic_prop( hp_func="mu_hp_func", up_func="mu_up_func", sp_func="mu_sp_func", tp_liq_func="mu_liq_tp_func", tp_vap_func="mu_vap_tp_func", mass_uc=self.param.uc["uPa to Pa"], mole_uc=self.param.uc["uPa to Pa"], **kwargs, )
[docs] def viscosity_liq(self, **kwargs): """Liquid phase viscosity""" if not viscosity_available(self.param.pure_component): raise RuntimeError( f"Viscosity not available for {self.param.pure_component}" ) return self._generic_prop_phase( hp_func="mu_liq_hp_func", up_func="mu_liq_up_func", sp_func="mu_liq_sp_func", tp_func="mu_liq_tp_func", mass_uc=self.param.uc["uPa to Pa"], mole_uc=self.param.uc["uPa to Pa"], **kwargs, )
[docs] def viscosity_vap(self, **kwargs): """Vapor phase viscosity""" if not viscosity_available(self.param.pure_component): raise RuntimeError( f"Viscosity not available for {self.param.pure_component}" ) return self._generic_prop_phase( hp_func="mu_vap_hp_func", up_func="mu_vap_up_func", sp_func="mu_vap_sp_func", tp_func="mu_vap_tp_func", mass_uc=self.param.uc["uPa to Pa"], mole_uc=self.param.uc["uPa to Pa"], **kwargs, )
[docs] def thermal_conductivity(self, **kwargs): """Mixed phase thermal conductivity (value for two-phase is not meaningful) use the liquid or vapor version if two phases are expected. """ if not thermal_conductivity_available(self.param.pure_component): raise RuntimeError( f"Thermal conductivity not available for {self.param.pure_component}" ) return self._generic_prop( hp_func="lambda_hp_func", up_func="lambda_up_func", sp_func="lambda_sp_func", tp_liq_func="lambda_liq_tp_func", tp_vap_func="lambda_vap_tp_func", mass_uc=self.param.uc["mW to W"], mole_uc=self.param.uc["mW to W"], **kwargs, )
[docs] def thermal_conductivity_liq(self, **kwargs): """Liquid phase thermal conductivity""" if not thermal_conductivity_available(self.param.pure_component): raise RuntimeError( f"Thermal conductivity not available for {self.param.pure_component}" ) return self._generic_prop_phase( hp_func="lambda_liq_hp_func", up_func="lambda_liq_up_func", sp_func="lambda_liq_sp_func", tp_func="lambda_liq_tp_func", mass_uc=self.param.uc["mW to W"], mole_uc=self.param.uc["mW to W"], **kwargs, )
[docs] def thermal_conductivity_vap(self, **kwargs): """Vapor phase thermal conductivity""" if not thermal_conductivity_available(self.param.pure_component): raise RuntimeError( f"Thermal conductivity not available for {self.param.pure_component}" ) return self._generic_prop_phase( hp_func="lambda_vap_hp_func", up_func="lambda_vap_up_func", sp_func="lambda_vap_sp_func", tp_func="lambda_vap_tp_func", mass_uc=self.param.uc["mW to W"], mole_uc=self.param.uc["mW to W"], **kwargs, )
[docs] def surface_tension(self, **kwargs): """Surface tension, this is only meaningful for two-phase region""" if not surface_tension_available(self.param.pure_component): raise RuntimeError( f"Surface tension not available for {self.param.pure_component}" ) return self._generic_prop( hp_func="sigma_hp_func", up_func="sigma_up_func", sp_func="sigma_sp_func", tp_liq_func="sigma_liq_tp_func", tp_vap_func="sigma_vap_tp_func", mass_uc=self.param.uc["mN to N"], mole_uc=self.param.uc["mN to N"], **kwargs, )
[docs] def p_sat(self, T): """Return saturation pressure as a function of T or tau""" self.add_funcs(names=["p_sat_t_func"]) return ( self.blk.p_sat_t_func(self.param.pure_component, T, _data_dir) * self.param.uc["kPa to Pa"] )
[docs] def T_sat(self, p, convert_args=True): """Return saturation temperature as a function of p""" if convert_args: p *= self.param.uc["Pa to kPa"] self.add_funcs(names=["t_sat_func"]) return self.blk.t_sat_func(self.param.pure_component, p, _data_dir)
[docs] def h_vap_sat(self, T=None, p=None, result_basis=None, convert_args=True): """Return saturation vapor enthalpy as a function of T or p""" if result_basis is None: result_basis = self.amount_basis if T is not None: self.add_funcs(names=["h_vap_sat_t_func"]) h = self.blk.h_vap_sat_t_func(self.param.pure_component, T, _data_dir) elif p is not None: self.add_funcs(names=["h_vap_sat_p_func"]) if convert_args: p *= self.param.uc["Pa to kPa"] h = self.blk.h_vap_sat_p_func(self.param.pure_component, p, _data_dir) if result_basis == AmountBasis.MOLE: return h * self.param.uc["kJ/kg to J/mol"] return h * self.param.uc["kJ/kg to J/kg"]
[docs] def h_liq_sat(self, T=None, p=None, result_basis=None, convert_args=True): """Return saturation liquid enthalpy as a function of T or p""" if result_basis is None: result_basis = self.amount_basis if T is not None: self.add_funcs(names=["h_liq_sat_t_func"]) h = self.blk.h_liq_sat_t_func(self.param.pure_component, T, _data_dir) elif p is not None: self.add_funcs(names=["h_liq_sat_p_func"]) if convert_args: p *= self.param.uc["Pa to kPa"] h = self.blk.h_liq_sat_p_func(self.param.pure_component, p, _data_dir) if result_basis == AmountBasis.MOLE: return h * self.param.uc["kJ/kg to J/mol"] return h * self.param.uc["kJ/kg to J/kg"]
[docs] def s_vap_sat(self, T=None, p=None, result_basis=None, convert_args=True): """Return saturation vapor entropy as a function of T or p""" if result_basis is None: result_basis = self.amount_basis if T is not None: self.add_funcs(names=["s_vap_sat_t_func"]) s = self.blk.s_vap_sat_t_func(self.param.pure_component, T, _data_dir) elif p is not None: self.add_funcs(names=["s_vap_sat_p_func"]) if convert_args: p *= self.param.uc["Pa to kPa"] s = self.blk.s_vap_sat_p_func(self.param.pure_component, p, _data_dir) if result_basis == AmountBasis.MOLE: return s * self.param.uc["kJ/kg/K to J/mol/K"] return s * self.param.uc["kJ/kg/K to J/kg/K"]
[docs] def s_liq_sat(self, T=None, p=None, result_basis=None, convert_args=True): """Return saturation liquid entropy as a function of T or p""" if result_basis is None: result_basis = self.amount_basis if T is not None: self.add_funcs(names=["s_liq_sat_t_func"]) s = self.blk.s_liq_sat_t_func(self.param.pure_component, T, _data_dir) elif p is not None: self.add_funcs(names=["s_liq_sat_p_func"]) if convert_args: p *= self.param.uc["Pa to kPa"] s = self.blk.s_liq_sat_p_func(self.param.pure_component, p, _data_dir) if result_basis == AmountBasis.MOLE: return s * self.param.uc["kJ/kg/K to J/mol/K"] return s * self.param.uc["kJ/kg/K to J/kg/K"]
[docs] def u_vap_sat(self, T=None, p=None, result_basis=None, convert_args=True): """Return saturation vapor internal energy as a function of T or p""" if result_basis is None: result_basis = self.amount_basis if T is not None: self.add_funcs(names=["u_vap_sat_t_func"]) u = self.blk.u_vap_sat_t_func(self.param.pure_component, T, _data_dir) elif p is not None: self.add_funcs(names=["u_vap_sat_p_func"]) if convert_args: p *= self.param.uc["Pa to kPa"] u = self.blk.u_vap_sat_p_func(self.param.pure_component, p, _data_dir) if result_basis == AmountBasis.MOLE: return u * self.param.uc["kJ/kg to J/mol"] return u * self.param.uc["kJ/kg to J/kg"]
[docs] def u_liq_sat(self, T=None, p=None, result_basis=None, convert_args=True): """Return saturation liquid internal energy as a function of T or p""" if result_basis is None: result_basis = self.amount_basis if T is not None: self.add_funcs(names=["u_liq_sat_t_func"]) u = self.blk.u_liq_sat_t_func(self.param.pure_component, T, _data_dir) elif p is not None: self.add_funcs(names=["u_liq_sat_p_func"]) if convert_args: p *= self.param.uc["Pa to kPa"] u = self.blk.u_liq_sat_p_func(self.param.pure_component, p, _data_dir) if result_basis == AmountBasis.MOLE: return u * self.param.uc["kJ/kg to J/mol"] return u * self.param.uc["kJ/kg to J/kg"]
[docs] def v_vap_sat(self, T=None, p=None, result_basis=None, convert_args=True): """Return saturation vapor volume as a function of T or p""" if result_basis is None: result_basis = self.amount_basis if T is not None: self.add_funcs(names=["v_vap_sat_t_func"]) v = self.blk.v_vap_sat_t_func(self.param.pure_component, T, _data_dir) elif p is not None: self.add_funcs(names=["v_vap_sat_p_func"]) if convert_args: p *= self.param.uc["Pa to kPa"] v = self.blk.v_vap_sat_p_func(self.param.pure_component, p, _data_dir) if result_basis == AmountBasis.MOLE: return v * self.param.uc["m3/kg to m3/mol"] return v
[docs] def v_liq_sat(self, T=None, p=None, result_basis=None, convert_args=True): """Return saturation liquid volume as a function of T or p""" if result_basis is None: result_basis = self.amount_basis if T is not None: self.add_funcs(names=["v_liq_sat_t_func"]) v = self.blk.v_liq_sat_t_func(self.param.pure_component, T, _data_dir) elif p is not None: self.add_funcs(names=["v_liq_sat_p_func"]) if convert_args: p *= self.param.uc["Pa to kPa"] v = self.blk.v_liq_sat_p_func(self.param.pure_component, p, _data_dir) if result_basis == AmountBasis.MOLE: return v * self.param.uc["m3/kg to m3/mol"] return v
[docs] @declare_process_block_class("HelmholtzParameterBlock") class HelmholtzParameterBlockData(PhysicalParameterBlock): """ This is a base class for Helmholtz equations of state using IDAES standard Helmholtz EOS external functions written in C++. """ CONFIG = PhysicalParameterBlock.CONFIG() CONFIG.declare( "pure_component", ConfigValue( default=None, domain=str, description="Pure chemical component", doc="(str) Pure component for which to calculate properties", ), ) CONFIG.declare( "phase_presentation", ConfigValue( default=PhaseType.MIX, domain=In(PhaseType), description="Set the way phases are presented to models", doc="""Set the way phases are presented to models. The MIX option appears to the framework to be a mixed phase containing liquid and/or vapor. The mixed option can simplify calculations at the unit model level since it can be treated as a single phase, but unit models such as flash vessels will not be able to treat the phases independently. The LG option presents as two separate phases to the framework. The L or G options can be used if it is known for sure that only one phase is present. **default** - PhaseType.MIX **Valid values:** { **PhaseType.MIX** - Present a mixed phase with liquid and/or vapor, **PhaseType.LG** - Present a liquid and vapor phase, **PhaseType.L** - Assume only liquid can be present, **PhaseType.G** - Assume only vapor can be present}""", ), ) CONFIG.declare( "state_vars", ConfigValue( default=StateVars.PH, domain=In(StateVars), description="State variable set", doc="""The set of state variables to use. Depending on the use, one state variable set or another may be better computationally. Usually pressure and enthalpy are the best choice because they are well behaved during a phase change. **default** - StateVars.PH **Valid values:** { **StateVars.PH** - Pressure-Enthalpy, **StateVars.PS** - Pressure-Entropy, **StateVars.PU** - Pressure-Internal Energy, **StateVars.TPX** - Temperature-Pressure-Quality}""", ), ) CONFIG.declare( "amount_basis", ConfigValue( default=AmountBasis.MOLE, domain=In(AmountBasis), description="Quantities on a mass or mole basis", doc="""The amount basis (mass or mole) for quantities **default** - AmountBasis.mole **Valid values:** { **AmountBasis.mole** - use mole units (mol), **AmountBasis.mass** - use mass units (kg)}""", ), )
[docs] def available(self): """Returns True if the shared library is installed and loads properly otherwise returns False """ return helmholtz_available()
def _suh_tpx( self, T=None, p=None, x=None, units=None, amount_basis=None, with_units=False, prop="h", ): """ Convenience function to calculate a state variable from temperature and either pressure or vapor fraction. This function can be used for inlet streams and initialization where temperature is known instead of a state variable. User must provide values for one of these sets of values: {T, P}, {T, x}, or {P, x}. Args: T: Temperature P: Pressure, None if saturated x: Vapor fraction [mol vapor/mol total] (between 0 and 1), None if superheated or sub-cooled units: The units to report the result in, if None use the default units appropriate for the amount basis. amount_basis (AmountBasis): Whether to use a mass or mole basis with_units (bool): if True return an expression with units prop (str): h, s, or u Returns: Total selected state variable. """ if amount_basis is None: amount_basis = self.config.amount_basis if units is None: if prop in ["h", "u"]: if amount_basis == AmountBasis.MOLE: units = pyo.units.J / pyo.units.mol else: units = pyo.units.J / pyo.units.kg else: if amount_basis == AmountBasis.MOLE: units = pyo.units.J / pyo.units.mol / pyo.units.K else: units = pyo.units.J / pyo.units.kg / pyo.units.K te = HelmholtzThermoExpressions(self, self, amount_basis=amount_basis) tmin = pyo.value(self.temperature_min) tmax = pyo.value(self.temperature_max) pmin = pyo.value(self.pressure_min) pmax = pyo.value(self.pressure_max) if not sum((p is None, T is None, x is None)) == 1: raise RuntimeError( "htpx function must be provided exactly two of the arguments T, p, x" ) if T is not None: T = pyo.units.convert(T, to_units=pyo.units.K) if not tmin <= pyo.value(T) <= tmax: raise RuntimeError(f"T = {pyo.value(T)}, ({tmin} K <= T <= {tmax} K)") if x is not None: if not 0 <= pyo.value(x) <= 1: raise RuntimeError(f"x = {pyo.value(x)}, (0 K <= x <= 1)") if p is not None: p = pyo.units.convert(p, to_units=pyo.units.Pa) if not pmin <= pyo.value(p) <= pmax: raise RuntimeError( f"p = {pyo.value(p)}, ({pmin} kPa <= p <= {pmax} kPa)" ) if T is not None: # P, T may be under-specified, but assume you know it's clearly a # vapor or liquid so figure out which and set x. psat = te.p_sat(T) if pyo.value(p) < pyo.value(psat): x = 1 else: x = 0 if prop == "h": if with_units: return pyo.value(pyo.units.convert(te.h(T=T, p=p, x=x), units)) * units return pyo.value(pyo.units.convert(te.h(T=T, p=p, x=x), units)) elif prop == "s": if with_units: return pyo.value(pyo.units.convert(te.s(T=T, p=p, x=x), units)) * units return pyo.value(pyo.units.convert(te.s(T=T, p=p, x=x), units)) elif prop == "u": if with_units: return pyo.value(pyo.units.convert(te.u(T=T, p=p, x=x), units)) * units return pyo.value(pyo.units.convert(te.u(T=T, p=p, x=x), units))
[docs] def htpx( self, T=None, p=None, x=None, units=None, amount_basis=None, with_units=False, ): """ Convenience method to calculate enthalpy from temperature and either pressure or vapor fraction. This function can be used for inlet streams and initialization where temperature is known instead of enthalpy. User must provide values for one of these sets of values: {T, P}, {T, x}, or {P, x}. Args: T (float): Temperature P (float): Pressure, None if saturated x (float): Vapor fraction [mol vapor/mol total] (between 0 and 1), None if superheated or sub-cooled units (Units): The units to report the result in, if None use the default units appropriate for the amount basis. amount_basis (AmountBasis): Whether to use a mass or mole basis with_units (bool): if True return an expression with units Returns: float: Specific or molar enthalpy """ return self._suh_tpx( T=T, p=p, x=x, units=units, amount_basis=amount_basis, with_units=with_units, prop="h", )
[docs] def stpx( self, T=None, p=None, x=None, units=None, amount_basis=None, with_units=False, ): """ Convenience method to calculate entropy from temperature and either pressure or vapor fraction. This function can be used for inlet streams and initialization where temperature is known instead of entropy. User must provide values for one of these sets of values: {T, P}, {T, x}, or {P, x}. Args: T (float): Temperature P (float): Pressure, None if saturated x (float): Vapor fraction [mol vapor/mol total] (between 0 and 1), None if superheated or sub-cooled units (Units): The units to report the result in, if None use the default units appropriate for the amount basis. amount_basis (AmountBasis): Whether to use a mass or mole basis with_units (bool): if True return an expression with units Returns: float: Specific or molar entropy """ return self._suh_tpx( T=T, p=p, x=x, units=units, amount_basis=amount_basis, with_units=with_units, prop="s", )
[docs] def utpx( self, T=None, p=None, x=None, units=None, amount_basis=None, with_units=False, ): """ Convenience method to calculate internal energy from temperature and either pressure or vapor fraction. This function can be used for inlet streams and initialization where temperature is known instead of internal energy. User must provide values for one of these sets of values: {T, P}, {T, x}, or {P, x}. Args: T (float): Temperature P (float): Pressure, None if saturated x (float): Vapor fraction [mol vapor/mol total] (between 0 and 1), None if superheated or sub-cooled units (Units): The units to report the result in, if None use the default units appropriate for the amount basis. amount_basis (AmountBasis): Whether to use a mass or mole basis with_units (bool): if True return an expression with units Returns: float: Specific or molar internal energy """ return self._suh_tpx( T=T, p=p, x=x, units=units, amount_basis=amount_basis, with_units=with_units, prop="u", )
def _set_default_scaling(self): """Set default scaling parameters to be used if not otherwise set""" # TODO<jce> leaving flow to not break things, but plan to remove it and # make the user specify with no default. self.set_default_scaling("flow_mol", 1e-4) self.set_default_scaling("flow_mass", 1) self.set_default_scaling("flow_mol_comp", 1e-4) self.set_default_scaling("flow_mass_comp", 1) self.set_default_scaling("flow_vol", 100) self.set_default_scaling("flow_mass", 1) # Set some scaling with reasonable a priori values self.set_default_scaling("temperature_crit", 1e-2) self.set_default_scaling("temperature_star", 1e-2) self.set_default_scaling("enth_mol", 1e-3) self.set_default_scaling("enth_mass", 1e-3) self.set_default_scaling("temperature", 1e-1) self.set_default_scaling("pressure", 1e-6) self.set_default_scaling("vapor_frac", 1e1) self.set_default_scaling("dens_mass_phase", 1e-2, index="Liq") self.set_default_scaling("dens_mass_phase", 1e1) # Not liq self.set_default_scaling("pressure_crit", 1e-6) self.set_default_scaling("dens_mass_crit", 1e-2) self.set_default_scaling("mw", 1e3) self.set_default_scaling("temperature_sat", 1e-2) self.set_default_scaling("pressure_sat", 1e-5) self.set_default_scaling("dens_mass_phase", 1e-2, index="Liq") self.set_default_scaling("dens_mass_phase", 1e1) self.set_default_scaling("enth_mol_sat_phase", 1e-2, index="Liq") self.set_default_scaling("enth_mol_sat_phase", 1e-4, index="Vap") self.set_default_scaling("enth_mass_sat_phase", 1e-2, index="Liq") self.set_default_scaling("enth_mass_sat_phase", 1e-4, index="Vap") self.set_default_scaling("dh_vap_mol", 1e-4) self.set_default_scaling("dh_vap_mass", 1e-4) self.set_default_scaling("energy_internal_mol_phase", 1e-2, index="Liq") self.set_default_scaling("energy_internal_mol_phase", 1e-4) self.set_default_scaling("energy_internal_mass_phase", 1e-2, index="Liq") self.set_default_scaling("energy_internal_mass_phase", 1e-4) self.set_default_scaling("enth_mol_phase", 1e-2, index="Liq") self.set_default_scaling("enth_mol_phase", 1e-4, index="Vap") self.set_default_scaling("entr_mol_phase", 1e-1, index="Liq") self.set_default_scaling("entr_mol_phase", 1e-1, index="Vap") self.set_default_scaling("enth_mass_phase", 1e-2, index="Liq") self.set_default_scaling("enth_mass_phase", 1e-4, index="Vap") self.set_default_scaling("entr_mass_phase", 1e-1, index="Liq") self.set_default_scaling("entr_mass_phase", 1e-1, index="Vap") self.set_default_scaling("cp_mol_phase", 1e-2, index="Liq") self.set_default_scaling("cp_mol_phase", 1e-2, index="Vap") self.set_default_scaling("cv_mol_phase", 1e-2, index="Liq") self.set_default_scaling("cv_mol_phase", 1e-2, index="Vap") self.set_default_scaling("cp_mass_phase", 1e-2, index="Liq") self.set_default_scaling("cp_mass_phase", 1e-2, index="Vap") self.set_default_scaling("cv_mass_phase", 1e-2, index="Liq") self.set_default_scaling("cv_mass_phase", 1e-2, index="Vap") self.set_default_scaling("dens_mol_phase", 1e-2, index="Liq") self.set_default_scaling("dens_mol_phase", 1e-4, index="Vap") self.set_default_scaling("phase_frac", 10) self.set_default_scaling("energy_internal_mol", 1e-3) self.set_default_scaling("energy_internal_mass", 1e-3) self.set_default_scaling("entr_mol", 1e-1) self.set_default_scaling("entr_mass", 1e-1) self.set_default_scaling("cp_mol", 1e-2) self.set_default_scaling("cv_mol", 1e-2) self.set_default_scaling("cp_mass", 1e-2) self.set_default_scaling("cv_mass", 1e-2) self.set_default_scaling("dens_mass", 1) self.set_default_scaling("dens_mol", 1e-3) self.set_default_scaling("heat_capacity_ratio", 1e1) def _create_component_and_phase_objects(self): # Create chemical component objects pp = self.config.phase_presentation for c in self.component_list: setattr(self, str(c), Component(_component_list_exists=True)) # Create phase objects if pp == PhaseType.MIX: self.private_phase_list = pyo.Set(initialize=["Vap", "Liq"]) self.Mix = Phase() elif pp == PhaseType.LG: self.private_phase_list = pyo.Set(initialize=["Vap", "Liq"]) self.Liq = LiquidPhase() self.Vap = VaporPhase() elif pp == PhaseType.L: self.private_phase_list = pyo.Set(initialize=["Liq"]) self.Liq = LiquidPhase() elif pp == PhaseType.G: self.private_phase_list = pyo.Set(initialize=["Vap"]) self.Vap = VaporPhase()
[docs] def build(self): """Populate the parameter block""" if not self.available(): raise RuntimeError("Helmholtz EoS external functions not available") super().build() # Check if the specified component is supported if not component_registered(self.config.pure_component): raise ConfigurationError( f"Component {self.config.pure_component} not supported." ) # This is imported here to avoid a circular import # pylint: disable-next=import-outside-toplevel from idaes.models.properties.general_helmholtz.helmholtz_state import ( HelmholtzStateBlock, ) self._state_block_class = HelmholtzStateBlock # set the component_list as required for the generic IDAES properties self.component_list = pyo.Set(initialize=[self.config.pure_component]) # since this a only a pure component package, have a specific # pure_component attribute self.pure_component = self.config.pure_component # State var set self.state_vars = self.config.state_vars # Phase equilibrium description self.phase_equilibrium_idx = (pyo.Set(initialize=[1]),) self.phase_equilibrium_list = ({1: ["H2O", ("Vap", "Liq")]},) # Add default scaling factors for property expressions or variables self._set_default_scaling() # Add idaes component and phase objects self._create_component_and_phase_objects() # To ensure consistency pull parameters from external functions add_helmholtz_external_functions( # Add parameter external functions self, [ # Constants "sgc_func", # specific gas constant "mw_func", # molecular weight # Critical properties "pc_func", # Critical pressure "tc_func", # Critical temperature "t_star_func", # Critical temperature "rhoc_func", # Critical density "rho_star_func", # Critical temperature # triple point properties "pt_func", # triple point pressure "tt_func", # triple point temperature "rhot_l_func", # triple point liquid density "rhot_v_func", # triple point vapor density # triple point properties "pt_func", # triple point pressure "tt_func", # triple point temperature "rhot_l_func", # triple point liquid density "rhot_v_func", # triple point vapor density # Bounds "pmin_func", # pmin "tmin_func", # tmin "pmax_func", # pmax "tmax_func", # tmax # "hlpt_func", "slpt_func", "ulpt_func", "hvpt_func", "svpt_func", "uvpt_func", ], ) # The parameters are constants and we don't want to call the external # functions more than once, so define Pyomo parameters with the values, # use the external function here to avoid defining the parameters twice pu = pyo.units cmp = self.pure_component self.add_param( "mw", pu.convert(self.mw_func(cmp, _data_dir), pu.kg / pu.mol), ) self.add_param( "sgc", pyo.units.convert(self.sgc_func(cmp, _data_dir), pu.J / pu.kg / pu.K), ) self.add_param( "sgc_mol", pu.convert(self.sgc * self.mw, pu.J / pu.mol / pu.K), ) self.add_param( "pressure_crit", pu.convert(self.pc_func(cmp, _data_dir), pu.Pa), ) self.add_param( "pressure_trip", pu.convert(self.pt_func(cmp, _data_dir), pu.Pa), ) self.add_param( "pressure_min", pu.convert(self.pmin_func(cmp, _data_dir), pu.Pa), ) self.add_param( "pressure_max", pu.convert(self.pmax_func(cmp, _data_dir), pu.Pa), ) self.add_param( "default_pressure_value", pu.convert((self.pressure_crit + self.pressure_trip) / 2.0, pu.Pa), ) self.default_pressure_bounds = (self.pressure_min, self.pressure_max) self.add_param( "temperature_star", pu.convert(self.t_star_func(cmp, _data_dir), pu.K), ) self.add_param( "temperature_crit", pu.convert(self.tc_func(cmp, _data_dir), pu.K), ) self.add_param( "temperature_trip", pu.convert(self.tt_func(cmp, _data_dir), pu.K), ) self.add_param( "temperature_min", pu.convert(self.tmin_func(cmp, _data_dir), pu.K), ) self.add_param( "temperature_max", pu.convert(self.tmax_func(cmp, _data_dir), pu.K), ) self.add_param( "default_temperature_value", pu.convert((self.temperature_crit + self.temperature_trip) / 2.0, pu.K), ) self.default_temperature_bounds = (self.temperature_min, self.temperature_max) self.add_param( "dens_mass_star", pu.convert(self.rho_star_func(cmp, _data_dir), pu.kg / pu.m**3), ) self.add_param( "dens_mol_star", pu.convert(self.dens_mass_star / self.mw, pu.mol / pu.m**3), ) self.add_param( "dens_mass_crit", pu.convert(self.rhoc_func(cmp, _data_dir), pu.kg / pu.m**3), ) self.add_param( "dens_mol_crit", pu.convert(self.dens_mass_crit / self.mw, pu.mol / pu.m**3), ) self.uc = { "J/mol to kJ/kg": (pyo.units.kJ / 1000 / pyo.units.J) / self.mw, "J/mol to J/kg": 1.0 / self.mw, "kJ/kg to J/mol": (pyo.units.J * 1000 / pyo.units.kJ) * self.mw, "J/mol/K to kJ/kg/K": (pyo.units.kJ / 1000 / pyo.units.J) / self.mw, "J/mol/K to J/kg/K": 1 / self.mw, "kJ/kg/K to J/mol/K": (pyo.units.J * 1000 / pyo.units.kJ) * self.mw, "J/kg to kJ/kg": (pyo.units.kJ / 1000 / pyo.units.J), "J/kg to J/mol": self.mw, "J/kg/K to J/mol/K": self.mw, "kJ/kg to J/kg": (pyo.units.J * 1000 / pyo.units.kJ), "J/kg/K to kJ/kg/K": (pyo.units.kJ / 1000 / pyo.units.J), "kJ/kg/K to J/kg/K": (pyo.units.J * 1000 / pyo.units.kJ), "kPa to Pa": (pyo.units.Pa * 1000 / pyo.units.kPa), "Pa to kPa": (pyo.units.kPa / 1000 / pyo.units.Pa), "kg/m3 to mol/m3": 1 / self.mw, "m3/kg to m3/mol": self.mw, "uPa to Pa": 1e-6 * pyo.units.Pa / pyo.units.uPa, "mW to W": 1e-3 * pyo.units.W / pyo.units.mW, "mN to N": 1e-3 * pyo.units.N / pyo.units.mN, } self.add_param( "enthalpy_mol_min", self.hlpt_func( cmp, self.pressure_trip * self.uc["Pa to kPa"], self.temperature_star / self.temperature_trip, _data_dir, ) * self.uc["kJ/kg to J/mol"], ) self.add_param( "enthalpy_mass_min", self.hlpt_func( cmp, self.pressure_trip * self.uc["Pa to kPa"], self.temperature_star / self.temperature_trip, _data_dir, ) * self.uc["kJ/kg to J/kg"], ) self.add_param( "enthalpy_mol_max", self.hlpt_func( cmp, self.pressure_max * self.uc["Pa to kPa"], self.temperature_star / self.temperature_max, _data_dir, ) * self.uc["kJ/kg to J/mol"], ) self.add_param( "enthalpy_mass_max", self.hlpt_func( cmp, self.pressure_max * self.uc["Pa to kPa"], self.temperature_star / self.temperature_max, _data_dir, ) * self.uc["kJ/kg to J/kg"], ) self.add_param("default_enthalpy_mass_value", self.enthalpy_mass_min) self.add_param("default_enthalpy_mol_value", self.enthalpy_mol_min) self.default_enthalpy_mass_bounds = ( self.enthalpy_mass_min, self.enthalpy_mass_max, ) self.default_enthalpy_mol_bounds = ( self.enthalpy_mol_min, self.enthalpy_mol_max, ) self.add_param( "entropy_mol_min", self.slpt_func( cmp, self.pressure_trip * self.uc["Pa to kPa"], self.temperature_star / self.temperature_trip, _data_dir, ) * self.uc["kJ/kg/K to J/mol/K"], ) self.add_param( "entropy_mass_min", self.slpt_func( cmp, self.pressure_trip * self.uc["Pa to kPa"], self.temperature_star / self.temperature_trip, _data_dir, ) * self.uc["kJ/kg/K to J/kg/K"], ) self.add_param( "entropy_mol_max", self.slpt_func( cmp, self.pressure_max * self.uc["Pa to kPa"], self.temperature_star / self.temperature_max, _data_dir, ) * self.uc["kJ/kg/K to J/mol/K"], ) self.add_param( "entropy_mass_max", self.slpt_func( cmp, self.pressure_max * self.uc["Pa to kPa"], self.temperature_star / self.temperature_max, _data_dir, ) * self.uc["kJ/kg/K to J/kg/K"], ) self.add_param("default_entropy_mass_value", self.entropy_mass_min) self.add_param("default_entropy_mol_value", self.entropy_mol_min) self.default_entropy_mass_bounds = ( self.entropy_mass_min, self.entropy_mass_max, ) self.default_entropy_mol_bounds = ( self.entropy_mol_min, self.entropy_mol_max, ) self.add_param( "energy_internal_mol_min", self.ulpt_func( cmp, self.pressure_trip * self.uc["Pa to kPa"], self.temperature_star / self.temperature_trip, _data_dir, ) * self.uc["kJ/kg to J/mol"], ) self.add_param( "energy_internal_mass_min", self.ulpt_func( cmp, self.pressure_trip * self.uc["Pa to kPa"], self.temperature_star / self.temperature_trip, _data_dir, ) * self.uc["kJ/kg to J/kg"], ) self.add_param( "energy_internal_mol_max", self.ulpt_func( cmp, self.pressure_max * self.uc["Pa to kPa"], self.temperature_star / self.temperature_max, _data_dir, ) * self.uc["kJ/kg to J/mol"], ) self.add_param( "energy_internal_mass_max", self.ulpt_func( cmp, self.pressure_max * self.uc["Pa to kPa"], self.temperature_star / self.temperature_max, _data_dir, ) * self.uc["kJ/kg to J/kg"], ) self.add_param( "default_energy_internal_mass_value", self.energy_internal_mass_min ) self.add_param( "default_energy_internal_mol_value", self.energy_internal_mol_min ) self.default_energy_internal_mass_bounds = ( self.energy_internal_mass_min, self.energy_internal_mass_max, ) self.default_energy_internal_mol_bounds = ( self.energy_internal_mol_min, self.energy_internal_mol_max, ) # Smoothing parameters for TPX complimentarity form if self.config.state_vars == StateVars.TPX: self.smoothing_pressure_over = pyo.Param( mutable=True, initialize=1e-4, doc="Smooth max parameter (pressure over)", units=pyo.units.Pa, ) self.smoothing_pressure_under = pyo.Param( mutable=True, initialize=1e-4, doc="Smooth max parameter (pressure under)", units=pyo.units.Pa, )
[docs] def add_param(self, name, expr): """Add a parameter to the block. Args: name (str): parameter name expr (expression): Pyomo expression for parameter value """ self.add_component( name, pyo.Param( initialize=pyo.value(expr), units=pyo.units.get_units(expr), domain=pyo.Reals, ), )
[docs] def initialize(self, *args, **kwargs): """No initialization required here. This method is included for compatibility. """
[docs] def dome_data( self, amount_basis=None, pressure_unit=pyo.units.kPa, energy_unit=pyo.units.kJ, mass_unit=pyo.units.kg, mol_unit=pyo.units.kmol, n=60, ): """Get data to plot the two-phase dome or saturation curve. This data can be used to plot the 2 phase dome for p-h and t-s diagrams and the saturation curve on the p-t diagram. Args: amount_bases (AmountBasis): Mass or mole basis. Get from parameter block if None. pressure_unit (PyomoUnit): Pressure units of measure energy_unit (PyomoUnit): Energy units of measure mass_unit (PyomoUnit): Mass units of measure mol_unit (PyomoUnit): Mole unit of measure Returns: dict: dictionary with the keys {'T', 'tau', 'p', 'delta_liq', 'delta_vap', 'h_liq', 'h_vap', 's_liq', 's_vap'} each a list of numbers corresponding to states along the two-phase dome. """ # Attach the external functions needed to this parameter block (self) as # Pyomo ExternalFunction. add_helmholtz_external_functions( self, [ "p_sat_func", "delta_sat_l_func", "delta_sat_v_func", "h_func", "s_func", ], ) # Set the amount basis, if not provided by the user get it from this # parameter block (self) if amount_basis is None: amount_basis = self.config.amount_basis # We'll do the calculation based on temperature. We want more points # near the critical point, so use log space to get that. The lower bound # for temperature is the triple point and the upper bound is the # critical point. Here temperature is as tau = T*/T. tau_c = pyo.value(self.temperature_star / self.temperature_crit) tau_t = pyo.value(self.temperature_star / self.temperature_trip) tau_dist_vec = np.logspace(-5, 0, n) tau_vec = [tau_c + td * (tau_t - tau_c) for td in tau_dist_vec] tau_vec = [tau_c] + tau_vec # Get pressures by calling the p_sat function from the temperature vector p_vec = [ pyo.value( pyo.units.convert( self.p_sat_func(self.pure_component, tau, _data_dir), pressure_unit ) ) for tau in tau_vec ] # Get the reduced density vector for the liquid, from this and # tau we can calculate all the rest of the properties for sat liquid delta_sat_l_vec = [ pyo.value(self.delta_sat_l_func(self.pure_component, tau, _data_dir)) for tau in tau_vec ] # Get the reduced density vector for the liquid, from this and # tau we can calculate all the rest of the properties for sat liquid delta_sat_v_vec = [ pyo.value(self.delta_sat_v_func(self.pure_component, tau, _data_dir)) for tau in tau_vec ] if amount_basis == AmountBasis.MOLE: s_liq_vec = [ pyo.value( pyo.units.convert( self.s_func(self.pure_component, delta, tau, _data_dir) * self.uc["kJ/kg/K to J/mol/K"], energy_unit / mol_unit / pyo.units.K, ) ) for delta, tau in zip(delta_sat_l_vec, tau_vec) ] s_vap_vec = [ pyo.value( pyo.units.convert( self.s_func(self.pure_component, delta, tau, _data_dir) * self.uc["kJ/kg/K to J/mol/K"], energy_unit / mol_unit / pyo.units.K, ) ) for delta, tau in zip(delta_sat_v_vec, tau_vec) ] h_liq_vec = [ pyo.value( pyo.units.convert( self.h_func(self.pure_component, delta, tau, _data_dir) * self.uc["kJ/kg to J/mol"], energy_unit / mol_unit, ) ) for delta, tau in zip(delta_sat_l_vec, tau_vec) ] h_vap_vec = [ pyo.value( pyo.units.convert( self.h_func(self.pure_component, delta, tau, _data_dir) * self.uc["kJ/kg to J/mol"], energy_unit / mol_unit, ) ) for delta, tau in zip(delta_sat_v_vec, tau_vec) ] else: s_liq_vec = [ pyo.value( pyo.units.convert( self.s_func(self.pure_component, delta, tau, _data_dir), energy_unit / mass_unit / pyo.units.K, ) ) for delta, tau in zip(delta_sat_l_vec, tau_vec) ] s_vap_vec = [ pyo.value( pyo.units.convert( self.s_func(self.pure_component, delta, tau, _data_dir), energy_unit / mass_unit / pyo.units.K, ) ) for delta, tau in zip(delta_sat_v_vec, tau_vec) ] h_liq_vec = [ pyo.value( pyo.units.convert( self.h_func(self.pure_component, delta, tau, _data_dir), energy_unit / mass_unit, ) ) for delta, tau in zip(delta_sat_l_vec, tau_vec) ] h_vap_vec = [ pyo.value( pyo.units.convert( self.h_func(self.pure_component, delta, tau, _data_dir), energy_unit / mass_unit, ) ) for delta, tau in zip(delta_sat_v_vec, tau_vec) ] return { "T": [pyo.value(self.temperature_star / tau) for tau in tau_vec], "tau": tau_vec, "p": p_vec, "delta_liq": delta_sat_l_vec, "delta_vap": delta_sat_v_vec, "h_liq": h_liq_vec, "h_vap": h_vap_vec, "s_liq": s_liq_vec, "s_vap": s_vap_vec, }
[docs] def isotherms(self, temperatures): """Get isotherm data for a P-H diagram. Args: temperatures: A list of temperatures Returns: dict: The keys are temperatures the values are dicts with "p", "h", "s", and "delta" data for the isotherm. """ add_helmholtz_external_functions( self, [ "p_sat_func", "delta_liq_func", "delta_vap_func", "h_func", "s_func", ], ) pt = pyo.value(pyo.units.convert(self.pressure_trip, pyo.units.kPa)) pc = pyo.value(pyo.units.convert(self.pressure_crit, pyo.units.kPa)) pmax = pyo.value(pyo.units.convert(self.pressure_max, pyo.units.kPa)) d = {} def _pvec(d, tau, p1, p2=None, phase="sat"): if phase == "sat": p_vec = [p1, p1] elif phase == "liq": dist_vec = np.logspace(-4, -0.25, 20) vec = [p1 + pd * (p2 - p1) for pd in dist_vec] p_vec = [p1] + vec else: dist_vec = np.logspace(-4, -0.3, 20) vec = [p1 + pd * (p2 - p1) for pd in dist_vec] p_vec = [p1] + vec + np.linspace(vec[-1], p2, 10).tolist() if phase == "liq" or phase == "sc": delta = [ pyo.value( self.delta_liq_func(self.pure_component, p, tau, _data_dir) ) for p in p_vec ] elif phase == "vap": delta = [ pyo.value( self.delta_vap_func(self.pure_component, p, tau, _data_dir) ) for p in p_vec ] else: # sat delta = [ pyo.value( self.delta_liq_func( self.pure_component, p_vec[0], tau, _data_dir ) ), pyo.value( self.delta_vap_func( self.pure_component, p_vec[1], tau, _data_dir ) ), ] h_vec = [ pyo.value(self.h_func(self.pure_component, dv, tau, _data_dir)) for dv in delta ] s_vec = [ pyo.value(self.s_func(self.pure_component, dv, tau, _data_dir)) for dv in delta ] d["p"] = p_vec d["delta"] = delta d["h"] = h_vec d["s"] = s_vec for t in temperatures: d[t] = {} d2 = d[t] tau = self.temperature_star / t for key in ["liq", "vap", "sat", "sc"]: d2[key] = {} if t >= pyo.value(self.temperature_crit): _pvec(d2["sc"], tau, pc, pmax, "sc") _pvec(d2["vap"], tau, pc, pt, "vap") else: p_sat = pyo.value(self.p_sat_func(self.pure_component, tau, _data_dir)) _pvec(d2["liq"], tau, p_sat, pmax, "liq") _pvec(d2["sat"], tau, p_sat, p_sat, "sat") _pvec(d2["vap"], tau, p_sat, pt, "vap") return d
[docs] def ph_diagram( self, ylim=None, xlim=None, points=None, figsize=None, dpi=None, isotherms=None, isotherms_line_format=None, isotherms_label=True, ): """Create a enthalpy-pressure diagram using Matplotlib Args: ylim (tuple): lower and upper limits for pressure axis xlim (tuple): lower and upper limits for enthalpy axis points (dict): dict of tuples points to label on the plot figsize (tuple): figure size dpi (int): figure dots per inch isotherms (list|None): list of temperatures for plotting isotherms isotherms_line_format (str|None): line format for isotherms isotherms_label (bool): if true label isotherms Returns: (figure, axis) """ if points is None: points = {} fig, ax = plt.subplots(figsize=figsize, dpi=dpi) if ylim is not None: ax.set_ylim(ylim) if xlim is not None: ax.set_xlim(xlim) # Get some parameters for plot limits pc = pyo.value(pyo.units.convert(self.pressure_crit, pyo.units.kPa)) pt = pyo.value(pyo.units.convert(self.pressure_trip, pyo.units.kPa)) tc = pyo.value(self.temperature_crit) ts = pyo.value(self.temperature_star) tt = pyo.value(self.temperature_trip) pmax = pyo.value(pyo.units.convert(self.pressure_max, pyo.units.kPa)) dome = self.dome_data() # plot saturaion curves use log scale for pressure ax.set_yscale("log") ax.plot(dome["h_liq"], dome["p"], c="b", label="sat liquid") ax.plot(dome["h_vap"], dome["p"], c="r", label="sat vapor") # Temperatures for isotherms t_vec = np.linspace( pyo.value(self.temperature_trip), pyo.value(self.temperature_crit), 6 ) p = {} h_l = {} h_v = {} if isotherms is not None: # plot isotherms if isinstance(isotherms, np.ndarray): t_vec = isotherms.tolist() elif isinstance(isotherms, (list, tuple)): t_vec = isotherms else: t_vec = np.linspace( pyo.value(self.temperature_trip), pyo.value(self.temperature_crit), 6, ).tolist() isotherm_data = self.isotherms(temperatures=t_vec) if isotherms_line_format is None: isotherms_line_format = {"c": "g"} for t, dat in isotherm_data.items(): for phase in ["sc", "liq", "vap", "sat"]: if dat[phase]: ax.plot( dat[phase]["h"], dat[phase]["p"], **isotherms_line_format ) if isotherms_label: for t, dat in isotherm_data.items(): if dat["sat"]: ppos = dat["sat"]["p"][0] hpos = (dat["sat"]["h"][0] + dat["sat"]["h"][1]) / 2.0 angle = -30 else: ppos = dat["vap"]["p"][0] hpos = dat["vap"]["h"][0] angle = -30 ax.text( hpos, ppos, f"{t:.2f} K", fontsize="small", ha="center", va="center", rotation=angle, ) x = [] y = [] for p, v in points.items(): ax.scatter([v[0]], [v[1]]) ax.text(v[0], v[1], p, ha="center", fontsize="xx-large") x.append(v[0]) y.append(v[1]) if len(x) > 1: # This closes the loop x.append(x[0]) y.append(y[0]) ax.plot(x, y, c="black") # Titles ax.set_title(f"P-H Diagram for {self.pure_component}") ax.set_xlabel("Enthalpy (kJ/kg)") ax.set_ylabel("Pressure (kPa)") return fig, ax
[docs] def ts_diagram(self, ylim=None, xlim=None, points=None, figsize=None, dpi=None): """Create a entropy-temperautre diagram using Matplotlib Args: ylim (tuple): lower and upper limits for temperature axis xlim (tuple): lower and upper limits for entropy axis points (dict): dict of tuples points to label on the plot figsize (tuple): figure size dpi (int): figure dots per inch Returns: (figure, axis) """ if points is None: points = {} add_helmholtz_external_functions( self, [ "p_sat_func", "tau_sat_func", "delta_sat_l_func", "delta_sat_v_func", "s_func", "slpt_func", "svpt_func", "delta_liq_func", "delta_vap_func", ], ) fig, ax = plt.subplots(figsize=figsize, dpi=dpi) if ylim is not None: ax.set_ylim(ylim) if xlim is not None: ax.set_xlim(xlim) # Get some parameters for plot limits pc = pyo.value(pyo.units.convert(self.pressure_crit, pyo.units.kPa)) pt = pyo.value(pyo.units.convert(self.pressure_trip, pyo.units.kPa)) tc = pyo.value(self.temperature_crit) ts = pyo.value(self.temperature_star) tt = pyo.value(self.temperature_trip) pmax = pyo.value(pyo.units.convert(self.pressure_max, pyo.units.kPa)) dome = self.dome_data() # plot saturaion curves use log scale for pressure # plt.yscale("log") ax.plot(dome["s_liq"], dome["T"], c="b", label="sat liquid") ax.plot(dome["s_vap"], dome["T"], c="r", label="sat vapor") x = [] y = [] for p, v in points.items(): ax.scatter([v[0]], [v[1]]) ax.text(v[0], v[1], p, ha="center", fontsize="xx-large") x.append(v[0]) y.append(v[1]) if len(x) > 1: x.append(x[0]) y.append(y[0]) plt.plot(x, y, c="black") ax.set_title(f"T-S Diagram for {self.pure_component}") ax.set_xlabel("Entropy (kJ/kg/K)") ax.set_ylabel("Temperature (K)") return fig, ax
[docs] def pt_diagram(self, ylim=None, xlim=None, figsize=None, dpi=None): """Create a pressure-teperature diagram using Matplotlib Args: ylim (tuple): lower and upper limits for pressure axis xlim (tuple): lower and upper limits for temperature axis figsize (tuple): figure size dpi (int): figure dots per inch Returns: (figure, axis) """ # Add external functions needed to plot PH-diagram add_helmholtz_external_functions( self, [ "p_sat_func", "tau_sat_func", "delta_sat_l_func", "delta_sat_v_func", "s_func", "slpt_func", "svpt_func", "delta_liq_func", "delta_vap_func", ], ) fig, ax = plt.subplots(figsize=figsize, dpi=dpi) if ylim is not None: ax.set_ylim(ylim) if xlim is not None: ax.set_xlim(xlim) # Get some parameters for plot limits pc = pyo.value(pyo.units.convert(self.pressure_crit, pyo.units.kPa)) pt = pyo.value(pyo.units.convert(self.pressure_trip, pyo.units.kPa)) tc = pyo.value(self.temperature_crit) ts = pyo.value(self.temperature_star) tt = pyo.value(self.temperature_trip) pmax = pyo.value(pyo.units.convert(self.pressure_max, pyo.units.kPa)) # Temperatures to plot the saturation from triple point to critical # in the form of tau (Tc/T) tau_sat_vec = np.linspace( 1, pyo.value(self.temperature_star) / (pyo.value(self.temperature_trip)), 200, ) p_sat_vec = [None] * len(tau_sat_vec) delta_sat_v_vec = [None] * len(tau_sat_vec) delta_sat_l_vec = [None] * len(tau_sat_vec) h_sat_v_vec = [None] * len(tau_sat_vec) h_sat_l_vec = [None] * len(tau_sat_vec) for i, tau in enumerate(tau_sat_vec): p_sat_vec[i] = pyo.value( self.p_sat_func(self.pure_component, tau, _data_dir) ) delta_sat_l_vec[i] = pyo.value( self.delta_sat_l_func(self.pure_component, tau, _data_dir) ) delta_sat_v_vec[i] = pyo.value( self.delta_sat_v_func(self.pure_component, tau, _data_dir) ) # plot saturaion curves use log scale for pressure ax.set_yscale("log") ax.plot(ts / tau_sat_vec, p_sat_vec, c="m", label="sat") ax.plot([tc, tc], [pc, pc * 10], c="m", label="sat") ax.plot([tc, tc * 1.1], [pc, pc], c="m", label="sat") # Points for critical point and triple point # plt.scatter([tc], [pc]) # plt.scatter([tt], [pt]) ax.set_title(f"P-T Diagram for {self.pure_component}") ax.set_ylabel("Pressure (kPa)") ax.set_xlabel("Temperature (K)") return fig, ax
# In case you can't remember the axis order in the diagrams hp_diagram = ph_diagram tp_diagram = pt_diagram st_diagram = ts_diagram
[docs] @classmethod def define_metadata(cls, obj): obj.add_properties( { "temperature_crit": {"method": None, "units": "K"}, "pressure_crit": {"method": None, "units": "Pa"}, "dens_mass_crit": {"method": None, "units": "kg/m^3"}, "dens_mol_crit": {"method": None, "units": "mol/m^3"}, "mw": {"method": None, "units": "kg/mol"}, "temperature_sat": {"method": "None", "units": "K"}, "flow_mol": {"method": None, "units": "mol/s"}, "flow_mass": {"method": None, "units": "kg/s"}, "flow_vol": {"method": None, "units": "m^3/s"}, "temperature": {"method": None, "units": "K"}, "pressure": {"method": None, "units": "Pa"}, "dens_mass_phase": {"method": None, "units": "kg/m^3"}, "temperature_red": {"method": None, "units": None}, "pressure_sat": {"method": None, "units": "kPa"}, "energy_internal_mol_phase": {"method": None, "units": "J/mol"}, "enth_mol_phase": {"method": None, "units": "J/mol"}, "entr_mol_phase": {"method": None, "units": "J/mol.K"}, "cp_mol_phase": {"method": None, "units": "J/mol.K"}, "cv_mol_phase": {"method": None, "units": "J/mol.K"}, "energy_internal_mass_phase": {"method": None, "units": "J/kg"}, "enth_mass_phase": {"method": None, "units": "J/kg"}, "entr_mass_phase": {"method": None, "units": "J/kg.K"}, "cp_mass_phase": {"method": None, "units": "J/kg.K"}, "cv_mass_phase": {"method": None, "units": "J/kg.K"}, "dens_mol_phase": {"method": None, "units": "mol/m^3"}, "therm_cond_phase": {"method": None, "units": "W/m.K"}, "surf_tens": {"method": None, "units": "N/m"}, "visc_d_phase": {"method": None, "units": "Pa.s"}, "visc_k_phase": {"method": None, "units": "m^2/s"}, "phase_frac": {"method": None, "units": None}, "flow_mol_comp": {"method": None, "units": "mol/s"}, "flow_mass_comp": {"method": None, "units": "kg/s"}, "energy_internal_mol": {"method": None, "units": "J/mol"}, "enth_mol": {"method": None, "units": "J/mol"}, "entr_mol": {"method": None, "units": "J/mol.K"}, "cp_mol": {"method": None, "units": "J/mol.K"}, "cv_mol": {"method": None, "units": "J/mol.K"}, "energy_internal_mass": {"method": None, "units": "J/kg"}, "enth_mass": {"method": None, "units": "J/kg"}, "entr_mass": {"method": None, "units": "J/kg.K"}, "cp_mass": {"method": None, "units": "J/kg.K"}, "cv_mass": {"method": None, "units": "J/kg.K"}, "heat_capacity_ratio": {"method": None, "units": None}, "dens_mass": {"method": None, "units": "kg/m^3"}, "dens_mol": {"method": None, "units": "mol/m^3"}, } ) obj.define_custom_properties( { "temperature_star": { "method": None, "units": obj.derived_units.TEMPERATURE, }, "dens_mass_star": { "method": None, "units": obj.derived_units.DENSITY_MASS, }, "dens_mol_star": { "method": None, "units": obj.derived_units.DENSITY_MOLE, }, "specific_gas_constant": { "method": None, "units": obj.derived_units.ENTROPY_MASS, }, "speed_sound": { "method": None, "units": obj.derived_units.VELOCITY, }, "vapor_frac": {"method": None, "units": pyo.units.dimensionless}, "dh_vap_mol": {"method": None, "units": obj.derived_units.ENERGY_MOLE}, "dh_vap_mass": {"method": None, "units": obj.derived_units.ENERGY_MASS}, } ) obj.add_default_units( { "time": pyo.units.s, "length": pyo.units.m, "mass": pyo.units.kg, "amount": pyo.units.mol, "temperature": pyo.units.K, } )