From a3f141f1d473d509d1768ab44ed821376f70479d Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 17 Apr 2024 15:52:33 -0400 Subject: [PATCH 01/33] Infrastructure work to support SmoothVLE2 --- .../base/generic_property.py | 490 +++------- .../modular_properties/base/tests/test_vle.py | 30 +- .../modular_properties/base/utility.py | 313 +++++- .../properties/modular_properties/eos/ceos.py | 97 +- .../examples/tests/test_BT_PR_SmoothVLE2.py | 891 ++++++++++++++++++ .../phase_equil/__init__.py | 1 + .../phase_equil/bubble_dew.py | 97 +- .../phase_equil/smooth_VLE.py | 9 +- .../phase_equil/smooth_VLE_2.py | 278 ++++++ .../state_definitions/FTPx.py | 6 +- 10 files changed, 1774 insertions(+), 438 deletions(-) create mode 100644 idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py create mode 100644 idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py diff --git a/idaes/models/properties/modular_properties/base/generic_property.py b/idaes/models/properties/modular_properties/base/generic_property.py index 0c1c74a46b..124ca96677 100644 --- a/idaes/models/properties/modular_properties/base/generic_property.py +++ b/idaes/models/properties/modular_properties/base/generic_property.py @@ -13,9 +13,6 @@ """ Framework for generic property packages """ -# TODO: Look into protected access issues -# pylint: disable=protected-access - # Import Pyomo libraries from pyomo.environ import ( Block, @@ -82,10 +79,14 @@ get_phase_method, GenericPropertyPackageError, StateIndex, + identify_VL_component_list, + estimate_Tbub, + estimate_Tdew, + estimate_Pbub, + estimate_Pdew, ) from idaes.models.properties.modular_properties.phase_equil.bubble_dew import ( LogBubbleDew, - _valid_VL_component_list as bub_dew_VL_comp_list, ) from idaes.models.properties.modular_properties.phase_equil.henry import HenryType @@ -1786,19 +1787,19 @@ def initialize( # Bubble temperature initialization if hasattr(k, "_mole_frac_tbub"): - blk._init_Tbub(k, T_units) + _init_Tbub(k, T_units) # Dew temperature initialization if hasattr(k, "_mole_frac_tdew"): - blk._init_Tdew(k, T_units) + _init_Tdew(k, T_units) # Bubble pressure initialization if hasattr(k, "_mole_frac_pbub"): - blk._init_Pbub(k, T_units) + _init_Pbub(k) # Dew pressure initialization if hasattr(k, "_mole_frac_pdew"): - blk._init_Pdew(k, T_units) + _init_Pdew(k) # Solve bubble, dew, and critical point constraints for c in k.component_objects(Constraint): @@ -2075,334 +2076,6 @@ def release_state(blk, flags, outlvl=idaeslog.NOTSET): init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="properties") init_log.info_high("State released.") - def _init_Tbub(self, blk, T_units): - for pp in blk.params._pe_pairs: - raoult_comps, henry_comps = _valid_VL_component_list(blk, pp) - - if raoult_comps == []: - continue - if henry_comps != []: - # Need to get liquid phase name - # TODO This logic probably needs to be updated to break - # when LLE is added - if blk.params.get_phase(pp[0]).is_liquid_phase(): - l_phase = pp[0] - else: - l_phase = pp[1] - - # Use lowest component temperature_crit as starting point - # Starting high and moving down generally works better, - # as it under-predicts next step due to exponential form of - # Psat. - # Subtract 1 to avoid potential singularities at Tcrit - Tbub0 = ( - min( - blk.params.get_component(j).temperature_crit.value - for j in raoult_comps - ) - - 1 - ) - - err = 1 - counter = 0 - - # Newton solver with step limiter to prevent overshoot - # Tolerance only needs to be ~1e-1 - # Iteration limit of 30 - while err > 1e-1 and counter < 30: - f = value( - sum( - get_method(blk, "pressure_sat_comp", j)( - blk, blk.params.get_component(j), Tbub0 * T_units - ) - * blk.mole_frac_comp[j] - for j in raoult_comps - ) - + sum( - blk.mole_frac_comp[j] - * blk.params.get_component(j) - .config.henry_component[l_phase]["method"] - .return_expression(blk, l_phase, j, Tbub0 * T_units) - for j in henry_comps - ) - - blk.pressure - ) - df = value( - sum( - get_method(blk, "pressure_sat_comp", j)( - blk, blk.params.get_component(j), Tbub0 * T_units, dT=True - ) - * blk.mole_frac_comp[j] - for j in raoult_comps - ) - + sum( - blk.mole_frac_comp[j] - * blk.params.get_component(j) - .config.henry_component[l_phase]["method"] - .dT_expression(blk, l_phase, j, Tbub0 * T_units) - for j in henry_comps - ) - ) - - # Limit temperature step to avoid excessive overshoot - if f / df < -50: - Tbub1 = Tbub0 + 50 - elif f / df > 50: - Tbub1 = Tbub0 - 50 - else: - Tbub1 = Tbub0 - f / df - - err = abs(Tbub1 - Tbub0) - Tbub0 = Tbub1 - counter += 1 - - blk.temperature_bubble[pp].value = Tbub0 - - for j in raoult_comps: - blk._mole_frac_tbub[pp, j].value = value( - blk.mole_frac_comp[j] - * get_method(blk, "pressure_sat_comp", j)( - blk, blk.params.get_component(j), Tbub0 * T_units - ) - / blk.pressure - ) - if blk.is_property_constructed("log_mole_frac_tbub"): - blk.log_mole_frac_tbub[pp, j].value = value( - log(blk._mole_frac_tbub[pp, j]) - ) - - for j in henry_comps: - blk._mole_frac_tbub[pp, j].value = value( - blk.mole_frac_comp[j] - * blk.params.get_component(j) - .config.henry_component[l_phase]["method"] - .return_expression(blk, l_phase, j, Tbub0 * T_units) - / blk.pressure - ) - if blk.is_property_constructed("log_mole_frac_tbub"): - blk.log_mole_frac_tbub[pp, j].value = value( - log(blk._mole_frac_tbub[pp, j]) - ) - - def _init_Tdew(self, blk, T_units): - for pp in blk.params._pe_pairs: - raoult_comps, henry_comps = _valid_VL_component_list(blk, pp) - - if raoult_comps == []: - continue - if henry_comps != []: - # Need to get liquid phase name - if blk.params.get_phase(pp[0]).is_liquid_phase(): - l_phase = pp[0] - else: - l_phase = pp[1] - - if ( - hasattr(blk, "_mole_frac_tbub") - and blk.temperature_bubble[pp].value is not None - ): - # If Tbub has been calculated above, use this as the - # starting point - Tdew0 = blk.temperature_bubble[pp].value - else: - # Otherwise, use lowest component critical temperature - # as starting point - # Subtract 1 to avoid potential singularities at Tcrit - Tdew0 = ( - min( - blk.params.get_component(j).temperature_crit.value - for j in raoult_comps - ) - - 1 - ) - - err = 1 - counter = 0 - - # Newton solver with step limiter to prevent overshoot - # Tolerance only needs to be ~1e-1 - # Iteration limit of 30 - while err > 1e-1 and counter < 30: - f = value( - blk.pressure - * ( - sum( - blk.mole_frac_comp[j] - / get_method(blk, "pressure_sat_comp", j)( - blk, blk.params.get_component(j), Tdew0 * T_units - ) - for j in raoult_comps - ) - + sum( - blk.mole_frac_comp[j] - / blk.params.get_component(j) - .config.henry_component[l_phase]["method"] - .return_expression(blk, l_phase, j, Tdew0 * T_units) - for j in henry_comps - ) - ) - - 1 - ) - df = -value( - blk.pressure - * ( - sum( - blk.mole_frac_comp[j] - / get_method(blk, "pressure_sat_comp", j)( - blk, blk.params.get_component(j), Tdew0 * T_units - ) - ** 2 - * get_method(blk, "pressure_sat_comp", j)( - blk, - blk.params.get_component(j), - Tdew0 * T_units, - dT=True, - ) - for j in raoult_comps - ) - + sum( - blk.mole_frac_comp[j] - / blk.params.get_component(j) - .config.henry_component[l_phase]["method"] - .return_expression(blk, l_phase, j, Tdew0 * T_units) - ** 2 - * blk.params.get_component(j) - .config.henry_component[l_phase]["method"] - .dT_expression(blk, l_phase, j, Tdew0 * T_units) - for j in henry_comps - ) - ) - ) - - # Limit temperature step to avoid excessive overshoot - if f / df < -50: - Tdew1 = Tdew0 + 50 - elif f / df > 50: - Tdew1 = Tdew0 - 50 - else: - Tdew1 = Tdew0 - f / df - - err = abs(Tdew1 - Tdew0) - Tdew0 = Tdew1 - counter += 1 - - blk.temperature_dew[pp].value = Tdew0 - - for j in raoult_comps: - blk._mole_frac_tdew[pp, j].value = value( - blk.mole_frac_comp[j] - * blk.pressure - / get_method(blk, "pressure_sat_comp", j)( - blk, blk.params.get_component(j), Tdew0 * T_units - ) - ) - if blk.is_property_constructed("log_mole_frac_tdew"): - blk.log_mole_frac_tdew[pp, j].value = value( - log(blk._mole_frac_tdew[pp, j]) - ) - for j in henry_comps: - blk._mole_frac_tdew[pp, j].value = value( - blk.mole_frac_comp[j] - * blk.pressure - / blk.params.get_component(j) - .config.henry_component[l_phase]["method"] - .return_expression(blk, l_phase, j, Tdew0 * T_units) - ) - if blk.is_property_constructed("log_mole_frac_tdew"): - blk.log_mole_frac_tdew[pp, j].value = value( - log(blk._mole_frac_tdew[pp, j]) - ) - - def _init_Pbub(self, blk, T_units): - for pp in blk.params._pe_pairs: - raoult_comps, henry_comps = _valid_VL_component_list(blk, pp) - - if raoult_comps == []: - continue - if henry_comps != []: - # Need to get liquid phase name - if blk.params.get_phase(pp[0]).is_liquid_phase(): - l_phase = pp[0] - else: - l_phase = pp[1] - - blk.pressure_bubble[pp].value = value( - sum( - blk.mole_frac_comp[j] * blk.pressure_sat_comp[j] - for j in raoult_comps - ) - + sum( - blk.mole_frac_comp[j] * blk.henry[l_phase, j] for j in henry_comps - ) - ) - - for j in raoult_comps: - blk._mole_frac_pbub[pp, j].value = value( - blk.mole_frac_comp[j] - * blk.pressure_sat_comp[j] - / blk.pressure_bubble[pp] - ) - if blk.is_property_constructed("log_mole_frac_pbub"): - blk.log_mole_frac_pbub[pp, j].value = value( - log(blk._mole_frac_pbub[pp, j]) - ) - for j in henry_comps: - blk._mole_frac_pbub[pp, j].value = value( - blk.mole_frac_comp[j] - * blk.henry[l_phase, j] - / blk.pressure_bubble[pp] - ) - if blk.is_property_constructed("log_mole_frac_pbub"): - blk.log_mole_frac_pbub[pp, j].value = value( - log(blk._mole_frac_pbub[pp, j]) - ) - - def _init_Pdew(self, blk, T_units): - for pp in blk.params._pe_pairs: - raoult_comps, henry_comps = _valid_VL_component_list(blk, pp) - - if raoult_comps == []: - continue - if henry_comps != []: - # Need to get liquid phase name - if blk.params.get_phase(pp[0]).is_liquid_phase(): - l_phase = pp[0] - else: - l_phase = pp[1] - - blk.pressure_dew[pp].value = value( - 1 - / ( - sum( - blk.mole_frac_comp[j] / blk.pressure_sat_comp[j] - for j in raoult_comps - ) - + sum( - blk.mole_frac_comp[j] / blk.henry[l_phase, j] - for j in henry_comps - ) - ) - ) - - for j in raoult_comps: - blk._mole_frac_pdew[pp, j].value = value( - blk.mole_frac_comp[j] - * blk.pressure_dew[pp] - / blk.pressure_sat_comp[j] - ) - if blk.is_property_constructed("log_mole_frac_pdew"): - blk.log_mole_frac_pdew[pp, j].value = value( - log(blk._mole_frac_pdew[pp, j]) - ) - for j in henry_comps: - blk._mole_frac_pdew[pp, j].value = value( - blk.mole_frac_comp[j] * blk.pressure_dew[pp] / blk.henry[l_phase, j] - ) - if blk.is_property_constructed("log_mole_frac_pdew"): - blk.log_mole_frac_pdew[pp, j].value = value( - log(blk._mole_frac_pdew[pp, j]) - ) - @declare_process_block_class("GenericStateBlock", block_class=_GenericStateBlock) class GenericStateBlockData(StateBlockData): @@ -5121,7 +4794,7 @@ def rule_log_mole_frac(b, p1, p2, j): henry_comps, l_only_comps, v_only_comps, - ) = bub_dew_VL_comp_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -5185,3 +4858,146 @@ def _initialize_critical_props(state_data): f"Make sure you have provided values for {prop} in all Component " "declarations." ) + + +def _init_Tbub(blk, T_units): + for pp in blk.params._pe_pairs: + l_phase, _, raoult_comps, henry_comps, _, _ = identify_VL_component_list( + blk, pp + ) + + if raoult_comps == []: + continue + + Tbub0 = estimate_Tbub(blk, T_units, raoult_comps, henry_comps, l_phase) + + blk.temperature_bubble[pp].set_value(Tbub0) + + for j in raoult_comps: + blk._mole_frac_tbub[pp, j].value = value( + blk.mole_frac_comp[j] + * get_method(blk, "pressure_sat_comp", j)( + blk, blk.params.get_component(j), Tbub0 * T_units + ) + / blk.pressure + ) + if blk.is_property_constructed("log_mole_frac_tbub"): + blk.log_mole_frac_tbub[pp, j].value = value( + log(blk._mole_frac_tbub[pp, j]) + ) + + for j in henry_comps: + blk._mole_frac_tbub[pp, j].value = value( + blk.mole_frac_comp[j] + * blk.params.get_component(j) + .config.henry_component[l_phase]["method"] + .return_expression(blk, l_phase, j, Tbub0 * T_units) + / blk.pressure + ) + if blk.is_property_constructed("log_mole_frac_tbub"): + blk.log_mole_frac_tbub[pp, j].value = value( + log(blk._mole_frac_tbub[pp, j]) + ) + + +def _init_Tdew(blk, T_units): + for pp in blk.params._pe_pairs: + l_phase, _, raoult_comps, henry_comps, _, _ = identify_VL_component_list( + blk, pp + ) + + if raoult_comps == []: + continue + + Tdew0 = estimate_Tdew(blk, T_units, raoult_comps, henry_comps, l_phase) + + blk.temperature_dew[pp].set_value(Tdew0) + + for j in raoult_comps: + blk._mole_frac_tdew[pp, j].value = value( + blk.mole_frac_comp[j] + * blk.pressure + / get_method(blk, "pressure_sat_comp", j)( + blk, blk.params.get_component(j), Tdew0 * T_units + ) + ) + if blk.is_property_constructed("log_mole_frac_tdew"): + blk.log_mole_frac_tdew[pp, j].value = value( + log(blk._mole_frac_tdew[pp, j]) + ) + for j in henry_comps: + blk._mole_frac_tdew[pp, j].value = value( + blk.mole_frac_comp[j] + * blk.pressure + / blk.params.get_component(j) + .config.henry_component[l_phase]["method"] + .return_expression(blk, l_phase, j, Tdew0 * T_units) + ) + if blk.is_property_constructed("log_mole_frac_tdew"): + blk.log_mole_frac_tdew[pp, j].value = value( + log(blk._mole_frac_tdew[pp, j]) + ) + + +def _init_Pbub(blk): + for pp in blk.params._pe_pairs: + l_phase, _, raoult_comps, henry_comps, _, _ = identify_VL_component_list( + blk, pp + ) + + if raoult_comps == []: + continue + + blk.pressure_bubble[pp].set_value( + estimate_Pbub(blk, raoult_comps, henry_comps, l_phase) + ) + + for j in raoult_comps: + blk._mole_frac_pbub[pp, j].value = value( + blk.mole_frac_comp[j] + * blk.pressure_sat_comp[j] + / blk.pressure_bubble[pp] + ) + if blk.is_property_constructed("log_mole_frac_pbub"): + blk.log_mole_frac_pbub[pp, j].value = value( + log(blk._mole_frac_pbub[pp, j]) + ) + for j in henry_comps: + blk._mole_frac_pbub[pp, j].value = value( + blk.mole_frac_comp[j] * blk.henry[l_phase, j] / blk.pressure_bubble[pp] + ) + if blk.is_property_constructed("log_mole_frac_pbub"): + blk.log_mole_frac_pbub[pp, j].value = value( + log(blk._mole_frac_pbub[pp, j]) + ) + + +def _init_Pdew(blk): + for pp in blk.params._pe_pairs: + l_phase, _, raoult_comps, henry_comps, _, _ = identify_VL_component_list( + blk, pp + ) + + if raoult_comps == []: + continue + + blk.pressure_dew[pp].set_value( + estimate_Pdew(blk, raoult_comps, henry_comps, l_phase) + ) + + for j in raoult_comps: + blk._mole_frac_pdew[pp, j].value = value( + blk.mole_frac_comp[j] * blk.pressure_dew[pp] / blk.pressure_sat_comp[j] + ) + if blk.is_property_constructed("log_mole_frac_pdew"): + blk.log_mole_frac_pdew[pp, j].value = value( + log(blk._mole_frac_pdew[pp, j]) + ) + for j in henry_comps: + blk._mole_frac_pdew[pp, j].value = value( + blk.mole_frac_comp[j] * blk.pressure_dew[pp] / blk.henry[l_phase, j] + ) + if blk.is_property_constructed("log_mole_frac_pdew"): + blk.log_mole_frac_pdew[pp, j].value = value( + log(blk._mole_frac_pdew[pp, j]) + ) diff --git a/idaes/models/properties/modular_properties/base/tests/test_vle.py b/idaes/models/properties/modular_properties/base/tests/test_vle.py index 79906693f5..417461ef69 100644 --- a/idaes/models/properties/modular_properties/base/tests/test_vle.py +++ b/idaes/models/properties/modular_properties/base/tests/test_vle.py @@ -34,6 +34,12 @@ ) from idaes.core.solvers import get_solver +from idaes.models.properties.modular_properties.base.generic_property import ( + _init_Pbub, + _init_Pdew, + _init_Tbub, + _init_Tdew, +) from idaes.models.properties.modular_properties.state_definitions import FTPx from idaes.models.properties.modular_properties.eos.ideal import Ideal from idaes.models.properties.modular_properties.phase_equil import SmoothVLE @@ -218,7 +224,7 @@ def test_build(self, model): @pytest.mark.unit def test_init_bubble_temperature(self, model): - model.props._init_Tbub(model.props[1], pyunits.K) + _init_Tbub(model.props[1], pyunits.K) assert pytest.approx(365.35, abs=0.01) == value( model.props[1].temperature_bubble[("Vap", "Liq")] @@ -232,7 +238,7 @@ def test_init_bubble_temperature(self, model): @pytest.mark.unit def test_init_dew_temperature(self, model): - model.props._init_Tdew(model.props[1], pyunits.K) + _init_Tdew(model.props[1], pyunits.K) assert pytest.approx(372.02, abs=0.01) == value( model.props[1].temperature_dew[("Vap", "Liq")] @@ -246,7 +252,7 @@ def test_init_dew_temperature(self, model): @pytest.mark.unit def test_init_bubble_pressure(self, model): - model.props._init_Pbub(model.props[1], pyunits.K) + _init_Pbub(model.props[1]) assert pytest.approx(109479, abs=1) == value( model.props[1].pressure_bubble[("Vap", "Liq")] @@ -260,7 +266,7 @@ def test_init_bubble_pressure(self, model): @pytest.mark.unit def test_init_dew_pressure(self, model): - model.props._init_Pdew(model.props[1], pyunits.K) + _init_Pdew(model.props[1]) assert pytest.approx(89820, abs=1) == value( model.props[1].pressure_dew[("Vap", "Liq")] @@ -524,7 +530,7 @@ def test_build(self, model): @pytest.mark.unit def test_init_bubble_temperature(self, model): - model.props._init_Tbub(model.props[1], pyunits.K) + _init_Tbub(model.props[1], pyunits.K) assert pytest.approx(365.35, abs=0.01) == value( model.props[1].temperature_bubble[("Vap", "Liq")] @@ -538,7 +544,7 @@ def test_init_bubble_temperature(self, model): @pytest.mark.unit def test_init_dew_temperature(self, model): - model.props._init_Tdew(model.props[1], pyunits.K) + _init_Tdew(model.props[1], pyunits.K) assert pytest.approx(372.02, abs=0.01) == value( model.props[1].temperature_dew[("Vap", "Liq")] @@ -552,7 +558,7 @@ def test_init_dew_temperature(self, model): @pytest.mark.unit def test_init_bubble_pressure(self, model): - model.props._init_Pbub(model.props[1], pyunits.K) + _init_Pbub(model.props[1]) assert pytest.approx(109479, abs=1) == value( model.props[1].pressure_bubble[("Vap", "Liq")] @@ -566,7 +572,7 @@ def test_init_bubble_pressure(self, model): @pytest.mark.unit def test_init_dew_pressure(self, model): - model.props._init_Pdew(model.props[1], pyunits.K) + _init_Pdew(model.props[1]) assert pytest.approx(89820, abs=1) == value( model.props[1].pressure_dew[("Vap", "Liq")] @@ -703,7 +709,7 @@ def test_build(self, model): @pytest.mark.unit def test_init_bubble_temperature(self, model): - model.props._init_Tbub(model.props[1], pyunits.K) + _init_Tbub(model.props[1], pyunits.K) assert pytest.approx(361.50, abs=0.01) == value( model.props[1].temperature_bubble[("Vap", "Liq")] @@ -720,7 +726,7 @@ def test_init_bubble_temperature(self, model): @pytest.mark.unit def test_init_dew_temperature(self, model): - model.props._init_Tdew(model.props[1], pyunits.K) + _init_Tdew(model.props[1], pyunits.K) assert pytest.approx(370.23, abs=0.01) == value( model.props[1].temperature_dew[("Vap", "Liq")] @@ -737,7 +743,7 @@ def test_init_dew_temperature(self, model): @pytest.mark.unit def test_init_bubble_pressure(self, model): - model.props._init_Pbub(model.props[1], pyunits.K) + _init_Pbub(model.props[1]) assert pytest.approx(118531, abs=1) == value( model.props[1].pressure_bubble[("Vap", "Liq")] @@ -754,7 +760,7 @@ def test_init_bubble_pressure(self, model): @pytest.mark.unit def test_init_dew_pressure(self, model): - model.props._init_Pdew(model.props[1], pyunits.K) + _init_Pdew(model.props[1]) assert pytest.approx(95056, abs=1) == value( model.props[1].pressure_dew[("Vap", "Liq")] diff --git a/idaes/models/properties/modular_properties/base/utility.py b/idaes/models/properties/modular_properties/base/utility.py index 8588de80fa..960b31adbd 100644 --- a/idaes/models/properties/modular_properties/base/utility.py +++ b/idaes/models/properties/modular_properties/base/utility.py @@ -19,12 +19,9 @@ # pylint: disable=missing-class-docstring # pylint: disable=missing-function-docstring -# TODO: Look into protected access issues -# pylint: disable=protected-access - from enum import Enum -from pyomo.environ import units as pyunits +from pyomo.environ import units as pyunits, value from idaes.core.util.exceptions import ( BurntToast, @@ -43,7 +40,10 @@ class StateIndex(Enum): class GenericPropertyPackageError(PropertyPackageError): - # Error message for when a property is called for but no option provided + """ + Error message for when a property is called for but no option provided + """ + def __init__(self, block, prop): super().__init__() self.prop = prop @@ -241,6 +241,19 @@ class ConcentrationForm(Enum): def get_concentration_term(blk, r_idx, log=False): + """ + Get necessary concentration terms for reactions from property package, allowing for + different bases. + + Args: + blk: StateBlock of interest + r_idx: index of reaction + log: whether to use log concentration of not + + Returns: + Var or Expression representing concentration + + """ cfg = blk.params.config if "rate_reactions" in cfg: try: @@ -288,3 +301,293 @@ def get_concentration_term(blk, r_idx, log=False): ) return conc_term + + +def identify_VL_component_list(blk, phase_pair): + """ + Identify liquid and vapor phases and which components are in VL equilibrium + + Args: + blk: StateBlock of interest + phase_pair: 2-tuple of Phases in equilibrium + + Returns: + Lists of component names for: + * liquid Phase object + * vapor Phase object + * components using Raoult's Law + * components using Henry's Law + * liquid only components, + * vapor only components + + """ + vl_comps = [] + henry_comps = [] + l_only_comps = [] + v_only_comps = [] + + pparams = blk.params + l_phase = None + v_phase = None + if pparams.get_phase(phase_pair[0]).is_liquid_phase(): + l_phase = phase_pair[0] + elif pparams.get_phase(phase_pair[0]).is_vapor_phase(): + v_phase = phase_pair[0] + + if pparams.get_phase(phase_pair[1]).is_liquid_phase(): + l_phase = phase_pair[1] + elif pparams.get_phase(phase_pair[1]).is_vapor_phase(): + v_phase = phase_pair[1] + + # Only need to do this for V-L pairs, so check + if l_phase is not None and v_phase is not None: + for j in blk.params.component_list: + if (l_phase, j) in blk.phase_component_set and ( + v_phase, + j, + ) in blk.phase_component_set: + cobj = pparams.get_component(j) + if cobj.config.henry_component is not None and ( + phase_pair[0] in cobj.config.henry_component + or phase_pair[1] in cobj.config.henry_component + ): + henry_comps.append(j) + else: + vl_comps.append(j) + elif (l_phase, j) in blk.phase_component_set: + l_only_comps.append(j) + elif (v_phase, j) in blk.phase_component_set: + v_only_comps.append(j) + else: + vl_comps = [] + henry_comps = [] + l_only_comps = [] + v_only_comps = [] + + return l_phase, v_phase, vl_comps, henry_comps, l_only_comps, v_only_comps + + +TOL = 1e-1 +MAX_ITER = 30 + + +def estimate_Tbub(blk, T_units, raoult_comps, henry_comps, liquid_phase): + """ + Function to estimate bubble point temperature + + Args: + blk: StateBlock to use + T_units: units of temperature + raoult_comps: list of components that follow Raoult's Law + henry_comps: list of components that follow Henry's Law + liquid_phase: name of liquid phase + + Returns: + Estimated bubble point temperature as a float. + + """ + # Use lowest component temperature_crit as starting point + # Starting high and moving down generally works better, + # as it under-predicts next step due to exponential form of + # Psat. + # Subtract 1 to avoid potential singularities at Tcrit + Tbub0 = ( + min(blk.params.get_component(j).temperature_crit.value for j in raoult_comps) + - 1 + ) + + err = 1 + counter = 0 + + # Newton solver with step limiter to prevent overshoot + # Tolerance only needs to be ~1e-1 + # Iteration limit of 30 + while err > TOL and counter < MAX_ITER: + f = value( + sum( + get_method(blk, "pressure_sat_comp", j)( + blk, blk.params.get_component(j), Tbub0 * T_units + ) + * blk.mole_frac_comp[j] + for j in raoult_comps + ) + + sum( + blk.mole_frac_comp[j] + * blk.params.get_component(j) + .config.henry_component[liquid_phase]["method"] + .return_expression(blk, liquid_phase, j, Tbub0 * T_units) + for j in henry_comps + ) + - blk.pressure + ) + df = value( + sum( + get_method(blk, "pressure_sat_comp", j)( + blk, blk.params.get_component(j), Tbub0 * T_units, dT=True + ) + * blk.mole_frac_comp[j] + for j in raoult_comps + ) + + sum( + blk.mole_frac_comp[j] + * blk.params.get_component(j) + .config.henry_component[liquid_phase]["method"] + .dT_expression(blk, liquid_phase, j, Tbub0 * T_units) + for j in henry_comps + ) + ) + + # Limit temperature step to avoid excessive overshoot + if f / df < -50: + Tbub1 = Tbub0 + 50 + elif f / df > 50: + Tbub1 = Tbub0 - 50 + else: + Tbub1 = Tbub0 - f / df + + err = abs(Tbub1 - Tbub0) + Tbub0 = Tbub1 + counter += 1 + + return Tbub0 + + +def estimate_Tdew(blk, T_units, raoult_comps, henry_comps, liquid_phase): + """ + Function to estimate dew point temperature + + Args: + blk: StateBlock to use + T_units: units of temperature + raoult_comps: list of components that follow Raoult's Law + henry_comps: list of components that follow Henry's Law + liquid_phase: name of liquid phase + + Returns: + Estimated dew point temperature as a float. + + """ + # Use lowest component critical temperature + # as starting point + # Subtract 1 to avoid potential singularities at Tcrit + Tdew0 = ( + min(blk.params.get_component(j).temperature_crit.value for j in raoult_comps) + - 1 + ) + + err = 1 + counter = 0 + + # Newton solver with step limiter to prevent overshoot + # Tolerance only needs to be ~1e-1 + # Iteration limit of 30 + while err > TOL and counter < MAX_ITER: + f = value( + blk.pressure + * ( + sum( + blk.mole_frac_comp[j] + / get_method(blk, "pressure_sat_comp", j)( + blk, blk.params.get_component(j), Tdew0 * T_units + ) + for j in raoult_comps + ) + + sum( + blk.mole_frac_comp[j] + / blk.params.get_component(j) + .config.henry_component[liquid_phase]["method"] + .return_expression(blk, liquid_phase, j, Tdew0 * T_units) + for j in henry_comps + ) + ) + - 1 + ) + df = -value( + blk.pressure + * ( + sum( + blk.mole_frac_comp[j] + / get_method(blk, "pressure_sat_comp", j)( + blk, blk.params.get_component(j), Tdew0 * T_units + ) + ** 2 + * get_method(blk, "pressure_sat_comp", j)( + blk, + blk.params.get_component(j), + Tdew0 * T_units, + dT=True, + ) + for j in raoult_comps + ) + + sum( + blk.mole_frac_comp[j] + / blk.params.get_component(j) + .config.henry_component[liquid_phase]["method"] + .return_expression(blk, liquid_phase, j, Tdew0 * T_units) + ** 2 + * blk.params.get_component(j) + .config.henry_component[liquid_phase]["method"] + .dT_expression(blk, liquid_phase, j, Tdew0 * T_units) + for j in henry_comps + ) + ) + ) + + # Limit temperature step to avoid excessive overshoot + if f / df < -50: + Tdew1 = Tdew0 + 50 + elif f / df > 50: + Tdew1 = Tdew0 - 50 + else: + Tdew1 = Tdew0 - f / df + + err = abs(Tdew1 - Tdew0) + Tdew0 = Tdew1 + counter += 1 + + return Tdew0 + + +def estimate_Pbub(blk, raoult_comps, henry_comps, liquid_phase): + """ + Function to estimate bubble point pressure + + Args: + blk: StateBlock to use + raoult_comps: list of components that follow Raoult's Law + henry_comps: list of components that follow Henry's Law + liquid_phase: name of liquid phase + + Returns: + Estimated bubble point pressure as a float. + + """ + return value( + sum(blk.mole_frac_comp[j] * blk.pressure_sat_comp[j] for j in raoult_comps) + + sum(blk.mole_frac_comp[j] * blk.henry[liquid_phase, j] for j in henry_comps) + ) + + +def estimate_Pdew(blk, raoult_comps, henry_comps, liquid_phase): + """ + Function to estimate dew point pressure + + Args: + blk: StateBlock to use + raoult_comps: list of components that follow Raoult's Law + henry_comps: list of components that follow Henry's Law + liquid_phase: name of liquid phase + + Returns: + Estimated dew point pressure as a float. + + """ + return value( + 1 + / ( + sum(blk.mole_frac_comp[j] / blk.pressure_sat_comp[j] for j in raoult_comps) + + sum( + blk.mole_frac_comp[j] / blk.henry[liquid_phase, j] for j in henry_comps + ) + ) + ) diff --git a/idaes/models/properties/modular_properties/eos/ceos.py b/idaes/models/properties/modular_properties/eos/ceos.py index d419ef149f..2298f6ebf5 100644 --- a/idaes/models/properties/modular_properties/eos/ceos.py +++ b/idaes/models/properties/modular_properties/eos/ceos.py @@ -18,9 +18,6 @@ # TODO: Missing docstrings # pylint: disable=missing-function-docstring -# TODO: Look into protected access issues -# pylint: disable=protected-access - from enum import Enum from copy import deepcopy @@ -459,6 +456,35 @@ def rule_delta_eq(m, p1, p2, p3, i): ), ) + # Calculate cubic coefficients + def calculate_cubic_coefficients(b, p1, p2, p3): + """ + Calculates the coefficients b, c, and d of the cubic + 0 = z**3 + b * z**2 + c * z + d + """ + + _A_eq = getattr(b, "_" + cname + "_A_eq") + _B_eq = getattr(b, "_" + cname + "_B_eq") + A_eq = _A_eq[p1, p2, p3] + B_eq = _B_eq[p1, p2, p3] + EoS_u = EoS_param[ctype]["u"] + EoS_w = EoS_param[ctype]["w"] + + _b = -(1 + B_eq - EoS_u * B_eq) + _c = A_eq - EoS_u * B_eq - EoS_u * B_eq**2 + EoS_w * B_eq**2 + _d = -(A_eq * B_eq + EoS_w * B_eq**2 + EoS_w * B_eq**3) + return (_b, _c, _d) + + def second_derivative(b, p1, p2, p3): + _b, _, _ = calculate_cubic_coefficients(b, p1, p2, p3) + z = b.compress_fact_phase[p3] + return 6 * z + 2 * _b + + b.add_component( + "_" + cname + "_cubic_second_derivative", + Expression(b.params._pe_pairs, b.phase_list, rule=second_derivative), + ) + @staticmethod def calculate_scaling_factors(b, pobj): pass @@ -1356,26 +1382,79 @@ def a(k): # ----------------------------------------------------------------------------- # Default rules for cubic expressions def func_fw_PR(cobj): + """ + fw function for Peng-Robinson EoS. + + Args: + cobj: Component object + + Returns: + expression for fw + + """ return 0.37464 + 1.54226 * cobj.omega - 0.26992 * cobj.omega**2 def func_fw_SRK(cobj): + """ + fw function for SRK EoS. + + Args: + cobj: Component object + + Returns: + expression for fw + + """ return 0.48 + 1.574 * cobj.omega - 0.176 * cobj.omega**2 def func_alpha_soave(T, fw, cobj): + """ + alpha function for SRK EoS. + + Args: + fw: expression for fw + cobj: Component object + + Returns: + expression for alpha + + """ Tc = cobj.temperature_crit Tr = T / Tc return (1 + fw * (1 - sqrt(Tr))) ** 2 def func_dalpha_dT_soave(T, fw, cobj): + """ + Function to get first derivative of alpha for SRK EoS. + + Args: + fw: expression for fw + cobj: Component object + + Returns: + expression for first derivative of alpha + + """ Tc = cobj.temperature_crit Tr = T / Tc return 1 / Tc * (-fw / sqrt(Tr)) * (1 + fw * (1 - sqrt(Tr))) def func_d2alpha_dT2_soave(T, fw, cobj): + """ + Function to get 2nd derivative of alpha for SRK EoS. + + Args: + fw: expression for fw + cobj: Component object + + Returns: + expression for 2nd derivative of alpha + + """ Tc = cobj.temperature_crit Tr = T / Tc return 1 / Tc**2 * ((fw**2 + fw) / (2 * Tr * sqrt(Tr))) @@ -1384,6 +1463,9 @@ def func_d2alpha_dT2_soave(T, fw, cobj): # ----------------------------------------------------------------------------- # Mixing rules def rule_am_default(m, cname, a, p, pp=()): + """ + Standard mixing rule for a term + """ k = getattr(m.params, cname + "_kappa") return sum( sum( @@ -1398,6 +1480,9 @@ def rule_am_default(m, cname, a, p, pp=()): def rule_am_crit_default(m, cname, a_crit): + """ + Standard mixing rule for a term at critical point + """ k = getattr(m.params, cname + "_kappa") return sum( sum( @@ -1412,8 +1497,14 @@ def rule_am_crit_default(m, cname, a_crit): def rule_bm_default(m, b, p): + """ + Standard mixing rule for b term + """ return sum(m.mole_frac_phase_comp[p, i] * b[i] for i in m.components_in_phase(p)) def rule_bm_crit_default(m, b): + """ + Standard mixing rule for b term at critical point + """ return sum(m.mole_frac_comp[i] * b[i] for i in m.component_list) diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py new file mode 100644 index 0000000000..a5374834fa --- /dev/null +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py @@ -0,0 +1,891 @@ +################################################################################# +# 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-2023 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. +################################################################################# +""" +Author: Andrew Lee +""" + +import pytest + +from pyomo.util.check_units import assert_units_consistent +from pyomo.environ import ( + check_optimal_termination, + ConcreteModel, + Objective, + units as pyunits, + value, +) + +from idaes.core import FlowsheetBlock +from idaes.models.properties.modular_properties.eos.ceos import cubic_roots_available +from idaes.models.properties.modular_properties.base.generic_property import ( + GenericParameterBlock, +) +from idaes.core.solvers import get_solver +import idaes.core.util.scaling as iscale +from idaes.models.properties.tests.test_harness import PropertyTestHarness +from idaes.core import LiquidPhase, VaporPhase, Component +from idaes.models.properties.modular_properties.state_definitions import FTPx +from idaes.models.properties.modular_properties.eos.ceos import Cubic, CubicType +from idaes.models.properties.modular_properties.phase_equil import SmoothVLE2 +from idaes.models.properties.modular_properties.phase_equil.bubble_dew import ( + LogBubbleDew, +) +from idaes.models.properties.modular_properties.phase_equil.forms import log_fugacity +from idaes.models.properties.modular_properties.pure import RPP4 + +import idaes.logger as idaeslog + +SOUT = idaeslog.INFO + +# Set module level pyest marker +pytestmark = pytest.mark.cubic_root + + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() +# Limit iterations to make sure sweeps aren';'t getting out of hand +solver.options["max_iter"] = 50 + +# --------------------------------------------------------------------- +# Configuration dictionary for an ideal Benzene-Toluene system + +# Data Sources: +# [1] The Properties of Gases and Liquids (1987) +# 4th edition, Chemical Engineering Series - Robert C. Reid +# [3] Engineering Toolbox, https://www.engineeringtoolbox.com +# Retrieved 1st December, 2019 + +configuration = { + # Specifying components + "components": { + "benzene": { + "type": Component, + "enth_mol_ig_comp": RPP4, + "entr_mol_ig_comp": RPP4, + "pressure_sat_comp": RPP4, + "phase_equilibrium_form": {("Vap", "Liq"): log_fugacity}, + "parameter_data": { + "mw": (78.1136e-3, pyunits.kg / pyunits.mol), # [1] + "pressure_crit": (48.9e5, pyunits.Pa), # [1] + "temperature_crit": (562.2, pyunits.K), # [1] + "omega": 0.212, # [1] + "cp_mol_ig_comp_coeff": { + "A": (-3.392e1, pyunits.J / pyunits.mol / pyunits.K), # [1] + "B": (4.739e-1, pyunits.J / pyunits.mol / pyunits.K**2), + "C": (-3.017e-4, pyunits.J / pyunits.mol / pyunits.K**3), + "D": (7.130e-8, pyunits.J / pyunits.mol / pyunits.K**4), + }, + "enth_mol_form_vap_comp_ref": (82.9e3, pyunits.J / pyunits.mol), # [3] + "entr_mol_form_vap_comp_ref": ( + -269, + pyunits.J / pyunits.mol / pyunits.K, + ), # [3] + "pressure_sat_comp_coeff": { + "A": (-6.98273, None), # [1] + "B": (1.33213, None), + "C": (-2.62863, None), + "D": (-3.33399, None), + }, + }, + }, + "toluene": { + "type": Component, + "enth_mol_ig_comp": RPP4, + "entr_mol_ig_comp": RPP4, + "pressure_sat_comp": RPP4, + "phase_equilibrium_form": {("Vap", "Liq"): log_fugacity}, + "parameter_data": { + "mw": (92.1405e-3, pyunits.kg / pyunits.mol), # [1] + "pressure_crit": (41e5, pyunits.Pa), # [1] + "temperature_crit": (591.8, pyunits.K), # [1] + "omega": 0.263, # [1] + "cp_mol_ig_comp_coeff": { + "A": (-2.435e1, pyunits.J / pyunits.mol / pyunits.K), # [1] + "B": (5.125e-1, pyunits.J / pyunits.mol / pyunits.K**2), + "C": (-2.765e-4, pyunits.J / pyunits.mol / pyunits.K**3), + "D": (4.911e-8, pyunits.J / pyunits.mol / pyunits.K**4), + }, + "enth_mol_form_vap_comp_ref": (50.1e3, pyunits.J / pyunits.mol), # [3] + "entr_mol_form_vap_comp_ref": ( + -321, + pyunits.J / pyunits.mol / pyunits.K, + ), # [3] + "pressure_sat_comp_coeff": { + "A": (-7.28607, None), # [1] + "B": (1.38091, None), + "C": (-2.83433, None), + "D": (-2.79168, None), + }, + }, + }, + }, + # Specifying phases + "phases": { + "Liq": { + "type": LiquidPhase, + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + "Vap": { + "type": VaporPhase, + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + }, + # Set base units of measurement + "base_units": { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + }, + # Specifying state definition + "state_definition": FTPx, + "state_bounds": { + "flow_mol": (0, 100, 1000, pyunits.mol / pyunits.s), + "temperature": (273.15, 300, 500, pyunits.K), + "pressure": (5e4, 1e5, 1e6, pyunits.Pa), + }, + "pressure_ref": (101325, pyunits.Pa), + "temperature_ref": (298.15, pyunits.K), + # Defining phase equilibria + "phases_in_equilibrium": [("Vap", "Liq")], + "phase_equilibrium_state": {("Vap", "Liq"): SmoothVLE2}, + "bubble_dew_method": LogBubbleDew, + "parameter_data": { + "PR_kappa": { + ("benzene", "benzene"): 0.000, + ("benzene", "toluene"): 0.000, + ("toluene", "benzene"): 0.000, + ("toluene", "toluene"): 0.000, + } + }, +} + + +@pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") +class TestBTPR(PropertyTestHarness): + def configure(self): + self.prop_pack = GenericParameterBlock + self.param_args = configuration + self.prop_args = {} + self.has_density_terms = False + + +# ----------------------------------------------------------------------------- +# Test robustness and some outputs +@pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") +class TestBTExample(object): + @pytest.fixture() + def m(self): + m = ConcreteModel() + + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.props = GenericParameterBlock(**configuration) + + m.fs.state = m.fs.props.build_state_block([1], defined_state=True) + + iscale.calculate_scaling_factors(m.fs.props) + iscale.calculate_scaling_factors(m.fs.state[1]) + return m + + @pytest.mark.integration + def test_T_sweep(self, m): + assert_units_consistent(m) + + m.fs.obj = Objective(expr=(m.fs.state[1].temperature - 510) ** 2) + m.fs.state[1].temperature.setub(600) + + for logP in [9.5, 10, 10.5, 11, 11.5, 12]: + m.fs.obj.deactivate() + + m.fs.state[1].flow_mol.fix(100) + m.fs.state[1].mole_frac_comp["benzene"].fix(0.5) + m.fs.state[1].mole_frac_comp["toluene"].fix(0.5) + m.fs.state[1].temperature.fix(300) + m.fs.state[1].pressure.fix(10 ** (0.5 * logP)) + + m.fs.state.initialize() + + m.fs.state[1].temperature.unfix() + m.fs.obj.activate() + + results = solver.solve(m) + + assert check_optimal_termination(results) + assert m.fs.state[1].flow_mol_phase["Liq"].value <= 1e-5 + + @pytest.mark.integration + def test_P_sweep(self, m): + for T in range(370, 500, 25): + m.fs.state[1].flow_mol.fix(100) + m.fs.state[1].mole_frac_comp["benzene"].fix(0.5) + m.fs.state[1].mole_frac_comp["toluene"].fix(0.5) + m.fs.state[1].temperature.fix(T) + m.fs.state[1].pressure.fix(1e5) + + m.fs.state.initialize() + + results = solver.solve(m) + + assert check_optimal_termination(results) + + while m.fs.state[1].pressure.value <= 1e6: + + results = solver.solve(m) + assert check_optimal_termination(results) + + m.fs.state[1].pressure.value = m.fs.state[1].pressure.value + 1e5 + + @pytest.mark.component + def test_T350_P1_x5(self, m): + m.fs.state[1].flow_mol.fix(100) + m.fs.state[1].mole_frac_comp["benzene"].fix(0.5) + m.fs.state[1].mole_frac_comp["toluene"].fix(0.5) + m.fs.state[1].temperature.fix(350) + m.fs.state[1].pressure.fix(1e5) + + # Trigger build of enthalpy and entropy + m.fs.state[1].enth_mol_phase + m.fs.state[1].entr_mol_phase + + m.fs.state.initialize(outlvl=SOUT) + + results = solver.solve(m) + + # Check for optimal solution + assert check_optimal_termination(results) + + assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), abs=1e-1) == 365 + assert 0.0035346 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Liq"]), 1e-5 + ) + assert 0.966749 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Vap"]), 1e-5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 0.894676 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.347566 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.971072 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.959791 + ) + + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 0.5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.70584 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.29416 + ) + + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Liq"]), 1e-5) == 38942.8 + ) + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Vap"]), 1e-5) == 78048.7 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Liq"]), 1e-5) == -361.794 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Vap"]), 1e-5) == -264.0181 + ) + + @pytest.mark.component + def test_T350_P5_x5(self, m): + m.fs.state[1].flow_mol.fix(100) + m.fs.state[1].mole_frac_comp["benzene"].fix(0.5) + m.fs.state[1].mole_frac_comp["toluene"].fix(0.5) + m.fs.state[1].temperature.fix(350) + m.fs.state[1].pressure.fix(5e5) + + # Trigger build of enthalpy and entropy + m.fs.state[1].enth_mol_phase + m.fs.state[1].entr_mol_phase + + m.fs.state.initialize(outlvl=SOUT) + + results = solver.solve(m) + + # Check for optimal solution + assert check_optimal_termination(results) + + assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 431.47 + assert ( + pytest.approx(value(m.fs.state[1].compress_fact_phase["Liq"]), 1e-5) + == 0.01766 + ) + assert ( + pytest.approx(value(m.fs.state[1].compress_fact_phase["Vap"]), 1e-5) + == 0.80245 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 0.181229 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.070601 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.856523 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.799237 + ) + + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 0.5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.65415 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.34585 + ) + + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Liq"]), 1e-5) == 38966.9 + ) + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Vap"]), 1e-5) == 75150.7 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Liq"]), 1e-5) == -361.8433 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Vap"]), 1e-5) == -281.9703 + ) + + @pytest.mark.component + def test_T450_P1_x5(self, m): + m.fs.state[1].flow_mol.fix(100) + m.fs.state[1].mole_frac_comp["benzene"].fix(0.5) + m.fs.state[1].mole_frac_comp["toluene"].fix(0.5) + m.fs.state[1].temperature.fix(450) + m.fs.state[1].pressure.fix(1e5) + + # Trigger build of enthalpy and entropy + m.fs.state[1].enth_mol_phase + m.fs.state[1].entr_mol_phase + + m.fs.state.initialize(outlvl=SOUT) + + results = solver.solve(m) + + # Check for optimal solution + assert check_optimal_termination(results) + + assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 371.4 + assert 0.0033583 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Liq"]), 1e-5 + ) + assert 0.9821368 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Vap"]), 1e-5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 8.069323 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 4.304955 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.985365 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.979457 + ) + + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 0.29861 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.70139 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.5 + ) + + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Liq"]), 1e-5) == 49441.2 + ) + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Vap"]), 1e-5) == 84175.1 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Liq"]), 1e-5) == -328.766 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Vap"]), 1e-5) == -241.622 + ) + + @pytest.mark.component + def test_T450_P5_x5(self, m): + m.fs.state[1].flow_mol.fix(100) + m.fs.state[1].mole_frac_comp["benzene"].fix(0.5) + m.fs.state[1].mole_frac_comp["toluene"].fix(0.5) + m.fs.state[1].temperature.fix(450) + m.fs.state[1].pressure.fix(5e5) + + # Trigger build of enthalpy and entropy + m.fs.state[1].enth_mol_phase + m.fs.state[1].entr_mol_phase + + m.fs.state.initialize(outlvl=SOUT) + + results = solver.solve(m) + + # Check for optimal solution + assert check_optimal_termination(results) + + assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 436.93 + assert 0.0166181 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Liq"]), 1e-5 + ) + assert 0.9053766 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Vap"]), 1e-5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 1.63308 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.873213 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.927534 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.898324 + ) + + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 0.3488737 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.6511263 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.5 + ) + + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Liq"]), 1e-5) == 51095.2 + ) + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Vap"]), 1e-5) == 83362.3 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Liq"]), 1e-5) == -326.299 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Vap"]), 1e-5) == -256.198 + ) + + @pytest.mark.component + def test_T368_P1_x5(self, m): + m.fs.state[1].flow_mol.fix(100) + m.fs.state[1].mole_frac_comp["benzene"].fix(0.5) + m.fs.state[1].mole_frac_comp["toluene"].fix(0.5) + m.fs.state[1].temperature.fix(368) + m.fs.state[1].pressure.fix(1e5) + + # Trigger build of enthalpy and entropy + m.fs.state[1].enth_mol_phase + m.fs.state[1].entr_mol_phase + + m.fs.state.initialize(outlvl=SOUT) + + results = solver.solve(m) + + # Check for optimal solution + assert check_optimal_termination(results) + + assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 368 + assert 0.003504 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Liq"]), 1e-5 + ) + assert 0.97 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Vap"]), 1e-5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 1.492049 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.621563 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.97469 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.964642 + ) + + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 0.4012128 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.5987872 + ) + + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.6141738 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.3858262 + ) + + m.fs.state[1].mole_frac_phase_comp.display() + m.fs.state[1].enth_mol_phase_comp.display() + + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Liq"]), 1e-5) == 38235.1 + ) + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Vap"]), 1e-5) == 77155.4 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Liq"]), 1e-5) == -359.256 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Vap"]), 1e-5) == -262.348 + ) + + @pytest.mark.component + def test_T376_P1_x2(self, m): + m.fs.state[1].flow_mol.fix(100) + m.fs.state[1].mole_frac_comp["benzene"].fix(0.2) + m.fs.state[1].mole_frac_comp["toluene"].fix(0.8) + m.fs.state[1].temperature.fix(376) + m.fs.state[1].pressure.fix(1e5) + + # Trigger build of enthalpy and entropy + m.fs.state[1].enth_mol_phase + m.fs.state[1].entr_mol_phase + + m.fs.state.initialize(outlvl=SOUT) + + results = solver.solve(m) + + # Check for optimal solution + assert check_optimal_termination(results) + + assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 376 + assert 0.00361333 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Liq"]), 1e-5 + ) + assert 0.968749 == pytest.approx( + value(m.fs.state[1].compress_fact_phase["Vap"]), 1e-5 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 1.8394188 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.7871415 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.9763608 + ) + assert ( + pytest.approx( + value(m.fs.state[1].fug_coeff_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.9663611 + ) + + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "benzene"]), 1e-5 + ) + == 0.17342 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Liq", "toluene"]), 1e-5 + ) + == 0.82658 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "benzene"]), 1e-5 + ) + == 0.3267155 + ) + assert ( + pytest.approx( + value(m.fs.state[1].mole_frac_phase_comp["Vap", "toluene"]), 1e-5 + ) + == 0.6732845 + ) + + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Liq"]), 1e-5) == 31535.8 + ) + assert ( + pytest.approx(value(m.fs.state[1].enth_mol_phase["Vap"]), 1e-5) == 69175.3 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Liq"]), 1e-5) == -369.033 + ) + assert ( + pytest.approx(value(m.fs.state[1].entr_mol_phase["Vap"]), 1e-5) == -273.513 + ) + + @pytest.mark.unit + def test_basic_scaling(self, m): + assert len(m.fs.state[1].scaling_factor) == 23 + assert m.fs.state[1].scaling_factor[m.fs.state[1].flow_mol] == 1e-2 + assert m.fs.state[1].scaling_factor[m.fs.state[1].flow_mol_phase["Liq"]] == 1e-2 + assert m.fs.state[1].scaling_factor[m.fs.state[1].flow_mol_phase["Vap"]] == 1e-2 + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1].flow_mol_phase_comp["Liq", "benzene"] + ] + == 1e-2 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1].flow_mol_phase_comp["Liq", "toluene"] + ] + == 1e-2 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1].flow_mol_phase_comp["Vap", "benzene"] + ] + == 1e-2 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1].flow_mol_phase_comp["Vap", "toluene"] + ] + == 1e-2 + ) + assert ( + m.fs.state[1].scaling_factor[m.fs.state[1].mole_frac_comp["benzene"]] + == 1000 + ) + assert ( + m.fs.state[1].scaling_factor[m.fs.state[1].mole_frac_comp["toluene"]] + == 1000 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1].mole_frac_phase_comp["Liq", "benzene"] + ] + == 1000 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1].mole_frac_phase_comp["Liq", "toluene"] + ] + == 1000 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1].mole_frac_phase_comp["Vap", "benzene"] + ] + == 1000 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1].mole_frac_phase_comp["Vap", "toluene"] + ] + == 1000 + ) + assert m.fs.state[1].scaling_factor[m.fs.state[1].pressure] == 1e-5 + assert m.fs.state[1].scaling_factor[m.fs.state[1].temperature] == 1e-2 + assert m.fs.state[1].scaling_factor[m.fs.state[1]._teq["Vap", "Liq"]] == 1e-2 + assert m.fs.state[1].scaling_factor[m.fs.state[1]._t1_Vap_Liq] == 1e-2 + + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1]._mole_frac_tbub["Vap", "Liq", "benzene"] + ] + == 1000 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1]._mole_frac_tbub["Vap", "Liq", "toluene"] + ] + == 1000 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1]._mole_frac_tdew["Vap", "Liq", "benzene"] + ] + == 1000 + ) + assert ( + m.fs.state[1].scaling_factor[ + m.fs.state[1]._mole_frac_tdew["Vap", "Liq", "toluene"] + ] + == 1000 + ) + assert ( + m.fs.state[1].scaling_factor[m.fs.state[1].temperature_bubble["Vap", "Liq"]] + == 1e-2 + ) + assert ( + m.fs.state[1].scaling_factor[m.fs.state[1].temperature_dew["Vap", "Liq"]] + == 1e-2 + ) diff --git a/idaes/models/properties/modular_properties/phase_equil/__init__.py b/idaes/models/properties/modular_properties/phase_equil/__init__.py index f6b207f0d6..53e32aba81 100644 --- a/idaes/models/properties/modular_properties/phase_equil/__init__.py +++ b/idaes/models/properties/modular_properties/phase_equil/__init__.py @@ -11,3 +11,4 @@ # for full copyright and license information. ################################################################################# from .smooth_VLE import SmoothVLE +from .smooth_VLE_2 import SmoothVLE2 diff --git a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py index 4bd953e686..a9f943cf2e 100644 --- a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py +++ b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py @@ -16,21 +16,16 @@ # TODO: Missing docstrings # pylint: disable=missing-function-docstring -# TODO: Look into protected access issues -# pylint: disable=protected-access - from pyomo.environ import Constraint from idaes.models.properties.modular_properties.base.utility import ( get_method, get_component_object as cobj, + identify_VL_component_list, ) import idaes.core.util.scaling as iscale from idaes.core.util.exceptions import ConfigurationError -# _valid_VL_component_list return variables that are not need in all cases -# pylint: disable=W0612 - class IdealBubbleDew: """Bubble and dew point calculations for ideal systems.""" @@ -54,7 +49,7 @@ def rule_bubble_temp(b, p1, p2): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -97,7 +92,7 @@ def rule_mole_frac_bubble_temp(b, p1, p2, j): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -141,7 +136,7 @@ def scale_temperature_bubble(b, overwrite=True): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, pp) + ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: continue elif v_only_comps != []: @@ -174,7 +169,7 @@ def rule_dew_temp(b, p1, p2): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -219,7 +214,7 @@ def rule_mole_frac_dew_temp(b, p1, p2, j): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -265,7 +260,7 @@ def scale_temperature_dew(b, overwrite=True): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, pp) + ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: continue elif v_only_comps != []: @@ -296,7 +291,7 @@ def rule_bubble_press(b, p1, p2): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -328,7 +323,7 @@ def rule_mole_frac_bubble_press(b, p1, p2, j): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -368,7 +363,7 @@ def scale_pressure_bubble(b, overwrite=True): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, pp) + ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: continue elif v_only_comps != []: @@ -401,7 +396,7 @@ def rule_dew_press(b, p1, p2): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -431,7 +426,7 @@ def rule_mole_frac_dew_press(b, p1, p2, j): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -471,7 +466,7 @@ def scale_pressure_dew(b, overwrite=True): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, pp) + ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: continue elif v_only_comps != []: @@ -505,7 +500,7 @@ def rule_bubble_temp(b, p1, p2, j): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -540,7 +535,7 @@ def rule_mole_frac_bubble_temp(b, p1, p2): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -570,7 +565,7 @@ def scale_temperature_bubble(b, overwrite=True): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, pp) + ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: continue elif v_only_comps != []: @@ -595,7 +590,7 @@ def rule_dew_temp(b, p1, p2, j): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -630,7 +625,7 @@ def rule_mole_frac_dew_temp(b, p1, p2): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -660,7 +655,7 @@ def scale_temperature_dew(b, overwrite=True): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, pp) + ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: continue elif v_only_comps != []: @@ -685,7 +680,7 @@ def rule_bubble_press(b, p1, p2, j): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -720,7 +715,7 @@ def rule_mole_frac_bubble_press(b, p1, p2): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -750,7 +745,7 @@ def scale_pressure_bubble(b, overwrite=True): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, pp) + ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: continue elif v_only_comps != []: @@ -775,7 +770,7 @@ def rule_dew_press(b, p1, p2, j): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -810,7 +805,7 @@ def rule_mole_frac_dew_press(b, p1, p2): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, (p1, p2)) + ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: # Not a VLE pair @@ -840,7 +835,7 @@ def scale_pressure_dew(b, overwrite=True): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, pp) + ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: continue elif v_only_comps != []: @@ -852,48 +847,6 @@ def scale_pressure_dew(b, overwrite=True): ) -def _valid_VL_component_list(blk, pp): - vl_comps = [] - henry_comps = [] - l_only_comps = [] - v_only_comps = [] - - pparams = blk.params - l_phase = None - v_phase = None - if pparams.get_phase(pp[0]).is_liquid_phase(): - l_phase = pp[0] - elif pparams.get_phase(pp[0]).is_vapor_phase(): - v_phase = pp[0] - - if pparams.get_phase(pp[1]).is_liquid_phase(): - l_phase = pp[1] - elif pparams.get_phase(pp[1]).is_vapor_phase(): - v_phase = pp[1] - - # Only need to do this for V-L pairs, so check - if l_phase is not None and v_phase is not None: - for j in blk.params.component_list: - if (l_phase, j) in blk.phase_component_set and ( - v_phase, - j, - ) in blk.phase_component_set: - cobj = pparams.get_component(j) - if cobj.config.henry_component is not None and ( - pp[0] in cobj.config.henry_component - or pp[1] in cobj.config.henry_component - ): - henry_comps.append(j) - else: - vl_comps.append(j) - elif (l_phase, j) in blk.phase_component_set: - l_only_comps.append(j) - elif (v_phase, j) in blk.phase_component_set: - v_only_comps.append(j) - - return l_phase, v_phase, vl_comps, henry_comps, l_only_comps, v_only_comps - - def _non_vle_phase_check(blk): if len(blk.phase_list) > 2: raise ConfigurationError( diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py index 4b2718b691..754c8d234a 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py @@ -21,14 +21,11 @@ # TODO: Missing docstrings # pylint: disable=missing-function-docstring -# TODO: Look into protected access issues -# pylint: disable=protected-access - from pyomo.environ import Constraint, Param, Var, value from idaes.core.util.exceptions import ConfigurationError from idaes.core.util.math import smooth_max, smooth_min -from idaes.models.properties.modular_properties.phase_equil.bubble_dew import ( - _valid_VL_component_list, +from idaes.models.properties.modular_properties.base.utility import ( + identify_VL_component_list, ) import idaes.core.util.scaling as iscale @@ -51,7 +48,7 @@ def phase_equil(b, phase_pair): _, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, phase_pair) + ) = identify_VL_component_list(b, phase_pair) if l_phase is None or v_phase is None: raise ConfigurationError( diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py new file mode 100644 index 0000000000..ee3ea3faa6 --- /dev/null +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -0,0 +1,278 @@ +################################################################################# +# 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-2023 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. +################################################################################# +""" +Implementation of the formulation proposed in: + +Dabadghao, V., Ghouse, J., Eslick, J., Lee, A., Burgard, A., Miller, D., Biegler, L., 2022, +A complementarity-based vapor-liquid equilibrium formulation for equation-oriented simulation +and optimization. AIChE Journal, DOI: 10.1002/aic.18029 +""" + +from pyomo.environ import Constraint, Param, units as pyunits, Var, value + +from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.math import smooth_min +from idaes.models.properties.modular_properties.base.utility import ( + identify_VL_component_list, +) +from idaes.models.properties.modular_properties.base.utility import ( + estimate_Tbub, + estimate_Tdew, +) +import idaes.core.util.scaling as iscale + + +# ----------------------------------------------------------------------------- +class SmoothVLE2: + """ + Improved Vapor-Liquid Equilibrium complementarity formulation for Cubic Equations of State. + """ + + @staticmethod + def phase_equil(b, phase_pair): + # This method is called via StateBlock.build, thus does not need clean-up + # try/except statements + suffix = "_" + phase_pair[0] + "_" + phase_pair[1] + + # Smooth VLE assumes a liquid and a vapor phase, so validate this + ( + l_phase, + v_phase, + vl_comps, + henry_comps, + l_only_comps, + v_only_comps, + ) = identify_VL_component_list(b, phase_pair) + + if l_phase is None or v_phase is None: + raise ConfigurationError( + f"{b.params.name} Generic Property Package phase pair {phase_pair[0]}-{phase_pair[1]} " + "was set to use Smooth VLE formulation, however this is not a vapor-liquid pair." + ) + + # Definition of equilibrium temperature for smooth VLE + uom = b.params.get_metadata().default_units + t_units = uom.TEMPERATURE + f_units = uom.AMOUNT / uom.TIME + + s = Var( + b.params.phase_list, + initialize=0.0, + bounds=(0, None), + doc="Slack variable for equilibrium temperature", + units=pyunits.dimensionless, + ) + b.add_component("s" + suffix, s) + + # Equilibrium temperature + def rule_teq(b): + if b.params.get_phase(phase_pair[0]).is_vapor_phase(): + vapor_phase = phase_pair[0] + liquid_phase = phase_pair[1] + else: + vapor_phase = phase_pair[1] + liquid_phase = phase_pair[0] + return ( + b._teq[phase_pair] + - b.temperature + - s[vapor_phase] * t_units + + s[liquid_phase] * t_units + == 0 + ) + + b.add_component("_tbar_constraint" + suffix, Constraint(rule=rule_teq)) + + eps = Param( + default=1e-04, + mutable=True, + doc="Smoothing parameter for complementarities", + units=f_units, + ) + b.add_component("eps" + suffix, eps) + + gp = Var( + b.params.phase_list, + initialize=0.0, + bounds=(0, None), + doc="Slack variable for cubic second derivative for phase p", + units=pyunits.dimensionless, + ) + b.add_component("gp" + suffix, gp) + + gn = Var( + b.params.phase_list, + initialize=0.0, + bounds=(0, None), + doc="Slack variable for cubic second derivative for phase p", + units=pyunits.dimensionless, + ) + b.add_component("gn" + suffix, gn) + + def rule_temperature_slack_complementarity(b, p): + flow_phase = b.flow_mol_phase[p] + + return smooth_min(s[p] * f_units, flow_phase, eps) == 0 + + b.add_component( + "temperature_slack_complementarity" + suffix, + Constraint( + b.params.phase_list, + rule=rule_temperature_slack_complementarity, + ), + ) + + def rule_cubic_root_complementarity(b, p): + p1, p2 = phase_pair + pobj = b.params.get_phase(p) + cname = pobj.config.equation_of_state_options["type"].name + cubic_second_derivative = getattr( + b, + "_" + cname + "_cubic_second_derivative", + ) + return cubic_second_derivative[p1, p2, p] == gp[p] - gn[p] + + b.add_component( + "cubic_root_complementarity" + suffix, + Constraint(b.params.phase_list, rule=rule_cubic_root_complementarity), + ) + + def rule_cubic_slack_complementarity(b, p): + flow_phase = b.flow_mol_phase[p] + if b.params.get_phase(p).is_vapor_phase(): + return smooth_min(gn[p] * f_units, flow_phase, eps) == 0 + else: + return smooth_min(gp[p] * f_units, flow_phase, eps) == 0 + + b.add_component( + "cubic_slack_complementarity" + suffix, + Constraint(b.params.phase_list, rule=rule_cubic_slack_complementarity), + ) + + @staticmethod + def calculate_scaling_factors(b, phase_pair): + suffix = "_" + phase_pair[0] + "_" + phase_pair[1] + sf_T = iscale.get_scaling_factor(b.temperature, default=1, warning=True) + + try: + teq_cons = getattr(b, "_teq_constraint" + suffix) + iscale.set_scaling_factor(b._teq[phase_pair], sf_T) + iscale.constraint_scaling_transform(teq_cons, sf_T, overwrite=False) + except AttributeError: + pass + + @staticmethod + def calculate_teq(blk, pp): + # --------------------------------------------------------------------- + # If present, initialize bubble and dew point calculations, and + # equilibrium temperature _teq + T_units = blk.params.get_metadata().default_units.TEMPERATURE + + liquid_phase, _, raoult_comps, henry_comps, _, _ = identify_VL_component_list( + blk, pp + ) + + Tbub = estimate_Tbub(blk, T_units, raoult_comps, henry_comps, liquid_phase) + Tdew = estimate_Tbub(blk, T_units, raoult_comps, henry_comps, liquid_phase) + + assert False + suffix = "_" + phase_pair[0] + "_" + phase_pair[1] + + if hasattr(b, "eq_temperature_bubble"): + _t1 = getattr(b, "_t1" + suffix) + _t1.value = max( + value(b.temperature), b.temperature_bubble[phase_pair].value + ) + else: + _t1 = b.temperature + + if hasattr(b, "eq_temperature_dew"): + b._teq[phase_pair].value = min( + _t1.value, b.temperature_dew[phase_pair].value + ) + else: + b._teq[phase_pair].value = _t1.value + + # --------------------------------------------------------------------- + # Initialize sV and sL slacks + _calculate_temperature_slacks(blk, pp) + + # --------------------------------------------------------------------- + # If flash, initialize g+ and g- slacks + _calculate_ceos_derivative_slacks(blk, pp) + + @staticmethod + def phase_equil_initialization(b, phase_pair): + suffix = "_" + phase_pair[0] + "_" + phase_pair[1] + + for c in b.component_objects(Constraint): + # Activate equilibrium constraints + if c.local_name in ("_teq_constraint" + suffix,): + c.deactivate() + + +def _calculate_temperature_slacks(b, phase_pair): + suffix = "_" + phase_pair[0] + "_" + phase_pair[1] + + s = getattr(b, "s" + suffix) + + if b.params.get_phase(phase_pair[0]).is_vapor_phase(): + vapor_phase = phase_pair[0] + liquid_phase = phase_pair[1] + else: + vapor_phase = phase_pair[1] + liquid_phase = phase_pair[0] + + if b._teq[phase_pair].value > b.temperature.value: + s[vapor_phase].value = value(b._teq[phase_pair] - b.temperature) + s[liquid_phase].value = 0 + elif b._teq[phase_pair].value < b.temperature.value: + s[vapor_phase].value = 0 + s[liquid_phase].value = value(b.temperature - b._teq[phase_pair]) + else: + s[vapor_phase].value = 0 + s[liquid_phase].value = 0 + + +def _calculate_ceos_derivative_slacks(b, phase_pair): + suffix = "_" + phase_pair[0] + "_" + phase_pair[1] + p1, p2 = phase_pair + + gp = getattr(b, "gp" + suffix) + gn = getattr(b, "gn" + suffix) + + if b.params.get_phase(phase_pair[0]).is_vapor_phase(): + vapor_phase = phase_pair[0] + liquid_phase = phase_pair[1] + else: + vapor_phase = phase_pair[1] + liquid_phase = phase_pair[0] + + vapobj = b.params.get_phase(vapor_phase) + liqobj = b.params.get_phase(liquid_phase) + cname_vap = vapobj.config.equation_of_state_options["type"].name + cname_liq = liqobj.config.equation_of_state_options["type"].name + cubic_second_derivative_vap = getattr( + b, "_" + cname_vap + "_cubic_second_derivative" + ) + cubic_second_derivative_liq = getattr( + b, "_" + cname_liq + "_cubic_second_derivative" + ) + + if value(cubic_second_derivative_liq[p1, p2, liquid_phase]) < 0: + gp[liquid_phase].value = 0 + gn[liquid_phase].value = value( + -cubic_second_derivative_liq[p1, p2, liquid_phase] + ) + if value(cubic_second_derivative_vap[p1, p2, vapor_phase]) > 0: + gp[vapor_phase].value = value(cubic_second_derivative_vap[p1, p2, vapor_phase]) + gn[vapor_phase].value = 0 diff --git a/idaes/models/properties/modular_properties/state_definitions/FTPx.py b/idaes/models/properties/modular_properties/state_definitions/FTPx.py index b89b9583af..95d1e2fc2f 100644 --- a/idaes/models/properties/modular_properties/state_definitions/FTPx.py +++ b/idaes/models/properties/modular_properties/state_definitions/FTPx.py @@ -36,8 +36,8 @@ get_method, GenericPropertyPackageError, ) -from idaes.models.properties.modular_properties.phase_equil.bubble_dew import ( - _valid_VL_component_list, +from idaes.models.properties.modular_properties.base.utility import ( + identify_VL_component_list, ) from idaes.models.properties.modular_properties.phase_equil.henry import ( HenryType, @@ -420,7 +420,7 @@ def state_initialization(b): henry_comps, l_only_comps, v_only_comps, - ) = _valid_VL_component_list(b, pp) + ) = identify_VL_component_list(b, pp) pp_VLE = pp if init_VLE: From ed3734a0c2ba1b25fcd03edffc66dbc3cf57e816 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 18 Apr 2024 09:47:18 -0400 Subject: [PATCH 02/33] Some pylint issues and tests for new cubic EoS functions --- .../base/generic_property.py | 11 ++- .../test_generic_property_integration.py | 1 + .../properties/modular_properties/eos/ceos.py | 84 +++++++++++-------- .../eos/tests/test_ceos_PR.py | 53 +++++++++++- .../phase_equil/bubble_dew.py | 4 +- .../phase_equil/smooth_VLE.py | 4 +- .../phase_equil/smooth_VLE_2.py | 2 + 7 files changed, 115 insertions(+), 44 deletions(-) diff --git a/idaes/models/properties/modular_properties/base/generic_property.py b/idaes/models/properties/modular_properties/base/generic_property.py index 124ca96677..4815e39643 100644 --- a/idaes/models/properties/modular_properties/base/generic_property.py +++ b/idaes/models/properties/modular_properties/base/generic_property.py @@ -13,6 +13,9 @@ """ Framework for generic property packages """ +# TODO: Pylint complains about variables with _x names as they are built by sub-classes +# pylint: disable=protected-access + # Import Pyomo libraries from pyomo.environ import ( Block, @@ -1334,19 +1337,19 @@ def initialization_routine( # Bubble temperature initialization if hasattr(k, "_mole_frac_tbub"): - model._init_Tbub(k, T_units) + _init_Tbub(k, T_units) # Dew temperature initialization if hasattr(k, "_mole_frac_tdew"): - model._init_Tdew(k, T_units) + _init_Tdew(k, T_units) # Bubble pressure initialization if hasattr(k, "_mole_frac_pbub"): - model._init_Pbub(k, T_units) + _init_Pbub(k) # Dew pressure initialization if hasattr(k, "_mole_frac_pdew"): - model._init_Pdew(k, T_units) + _init_Pdew(k) # Solve bubble, dew, and critical point constraints for c in k.component_objects(Constraint): diff --git a/idaes/models/properties/modular_properties/base/tests/test_generic_property_integration.py b/idaes/models/properties/modular_properties/base/tests/test_generic_property_integration.py index 1968f08176..f3b90da83e 100644 --- a/idaes/models/properties/modular_properties/base/tests/test_generic_property_integration.py +++ b/idaes/models/properties/modular_properties/base/tests/test_generic_property_integration.py @@ -217,6 +217,7 @@ def test_heater_w_inherent_rxns_comp_phase(self, frame): rel=1e-5, ) + frame.fs.H101.outlet.mole_frac_comp.display() assert value(frame.fs.H101.outlet.mole_frac_comp[0, "a"]) == pytest.approx( 1 / 6, rel=1e-5 ) diff --git a/idaes/models/properties/modular_properties/eos/ceos.py b/idaes/models/properties/modular_properties/eos/ceos.py index 2298f6ebf5..028a69e755 100644 --- a/idaes/models/properties/modular_properties/eos/ceos.py +++ b/idaes/models/properties/modular_properties/eos/ceos.py @@ -15,8 +15,8 @@ Currently only supports liquid and vapor phases """ -# TODO: Missing docstrings -# pylint: disable=missing-function-docstring +# TODO: Pylint complains about variables with _x names as they are built by other classes +# pylint: disable=protected-access from enum import Enum from copy import deepcopy @@ -449,41 +449,24 @@ def rule_delta_eq(m, p1, p2, p3, i): ) ) - b.add_component( - "_" + cname + "_delta_eq", - Expression( - b.params._pe_pairs, b.phase_component_set, rule=rule_delta_eq - ), - ) - - # Calculate cubic coefficients - def calculate_cubic_coefficients(b, p1, p2, p3): - """ - Calculates the coefficients b, c, and d of the cubic - 0 = z**3 + b * z**2 + c * z + d - """ - - _A_eq = getattr(b, "_" + cname + "_A_eq") - _B_eq = getattr(b, "_" + cname + "_B_eq") - A_eq = _A_eq[p1, p2, p3] - B_eq = _B_eq[p1, p2, p3] - EoS_u = EoS_param[ctype]["u"] - EoS_w = EoS_param[ctype]["w"] - - _b = -(1 + B_eq - EoS_u * B_eq) - _c = A_eq - EoS_u * B_eq - EoS_u * B_eq**2 + EoS_w * B_eq**2 - _d = -(A_eq * B_eq + EoS_w * B_eq**2 + EoS_w * B_eq**3) - return (_b, _c, _d) - - def second_derivative(b, p1, p2, p3): - _b, _, _ = calculate_cubic_coefficients(b, p1, p2, p3) - z = b.compress_fact_phase[p3] - return 6 * z + 2 * _b + b.add_component( + "_" + cname + "_delta_eq", + Expression(b.params._pe_pairs, b.phase_component_set, rule=rule_delta_eq), + ) - b.add_component( - "_" + cname + "_cubic_second_derivative", - Expression(b.params._pe_pairs, b.phase_list, rule=second_derivative), + def cubic_second_derivative_eq(b, p1, p2, p3): + b, _, _ = _calculate_equilibrium_cubic_coefficients( + b, cname, ctype, p1, p2, p3 ) + z = b.compress_fact_phase[p3] + return 6 * z + 2 * b + + b.add_component( + "_" + cname + "_cubic_second_derivative_eq", + Expression( + b.params._pe_pairs, b.phase_list, rule=cubic_second_derivative_eq + ), + ) @staticmethod def calculate_scaling_factors(b, pobj): @@ -1381,6 +1364,37 @@ def a(k): # ----------------------------------------------------------------------------- # Default rules for cubic expressions +def _calculate_equilibrium_cubic_coefficients(b, cubic_name, cubic_type, p1, p2, p3): + """ + Calculates the coefficients b, c, and d of the cubic 0 = z**3 + b * z**2 + c * z + d + at the equilibrium conditions + + Args: + b: StateBlock of interest + cubic_name: Name of Cubic EoS + cubic_type: Type of Cubic EoS + p1: Phase 1 + p2: Phase 2 + p3; Phase 3 + + Returns: + expressions for b, c, d + """ + + A_eq = getattr(b, "_" + cubic_name + "_A_eq") + B_eq = getattr(b, "_" + cubic_name + "_B_eq") + A_eq = A_eq[p1, p2, p3] + B_eq = B_eq[p1, p2, p3] + EoS_u = EoS_param[cubic_type]["u"] + EoS_w = EoS_param[cubic_type]["w"] + + b = -(1 + B_eq - EoS_u * B_eq) + c = A_eq - EoS_u * B_eq - EoS_u * B_eq**2 + EoS_w * B_eq**2 + d = -(A_eq * B_eq + EoS_w * B_eq**2 + EoS_w * B_eq**3) + + return b, c, d + + def func_fw_PR(cobj): """ fw function for Peng-Robinson EoS. diff --git a/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py b/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py index 2240c0160e..0de49fe4c1 100644 --- a/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py +++ b/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py @@ -41,7 +41,11 @@ ) from idaes.core.util.exceptions import PropertyNotSupportedError, ConfigurationError from idaes.core.util.constants import Constants as const -from idaes.models.properties.modular_properties.eos.ceos import cubic_roots_available +from idaes.models.properties.modular_properties.eos.ceos import ( + cubic_roots_available, + _calculate_equilibrium_cubic_coefficients, + EoS_param, +) from idaes.core.solvers import get_solver from idaes.core.initialization.initializer_base import InitializationStatus from idaes.models.properties.modular_properties.state_definitions import FTPx @@ -1076,6 +1080,53 @@ def test_vol_mol_phase_comp(m): ) +@pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") +@pytest.mark.unit +def test_calculate_equilibrium_cubic_coefficients(m): + b, c, d = _calculate_equilibrium_cubic_coefficients( + m.props[1], "PR", CubicType.PR, "Vap", "Liq", "Liq" + ) + + A_eq = m.props[1]._PR_A_eq["Vap", "Liq", "Liq"] + B_eq = m.props[1]._PR_B_eq["Vap", "Liq", "Liq"] + EoS_u = EoS_param["PR"]["u"] + EoS_w = EoS_param["PR"]["w"] + + assert str(b) == str(-(1 + B_eq - EoS_u * B_eq)) + assert str(c) == str(A_eq - EoS_u * B_eq - EoS_u * B_eq**2 + EoS_w * B_eq**2) + assert str(d) == str(-(A_eq * B_eq + EoS_w * B_eq**2 + EoS_w * B_eq**3)) + + +@pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") +@pytest.mark.unit +def test_cubic_second_derivative(m): + EoS_u = EoS_param["PR"]["u"] + + for k, v in m.props[1]._PR_cubic_second_derivative_eq.items(): + B_eq = m.props[1]._PR_B_eq[k] + b = -(1 + B_eq - EoS_u * B_eq) + z = m.props[1].compress_fact_phase[k[2]] + + assert str(v) == str(6 * z + 2 * b) + + +@pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") +@pytest.mark.unit +def test_calculate_equilibrium_cubic_coefficients(m): + b, c, d = _calculate_equilibrium_cubic_coefficients( + m.props[1], "PR", CubicType.PR, "Vap", "Liq", "Liq" + ) + + A_eq = m.props[1]._PR_A_eq["Vap", "Liq", "Liq"] + B_eq = m.props[1]._PR_B_eq["Vap", "Liq", "Liq"] + EoS_u = EoS_param["PR"]["u"] + EoS_w = EoS_param["PR"]["w"] + + assert str(b) == str(-(1 + B_eq - EoS_u * B_eq)) + assert str(c) == str(A_eq - EoS_u * B_eq - EoS_u * B_eq**2 + EoS_w * B_eq**2) + assert str(d) == str(-(A_eq * B_eq + EoS_w * B_eq**2 + EoS_w * B_eq**3)) + + class TestCEOSCriticalProps: @pytest.fixture(scope="class") def model(self): diff --git a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py index a9f943cf2e..16d0e10ef5 100644 --- a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py +++ b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py @@ -13,8 +13,8 @@ """ Modular methods for calculating bubble and dew points """ -# TODO: Missing docstrings -# pylint: disable=missing-function-docstring +# TODO: Pylint complains about variables with _x names as they are built by other classes +# pylint: disable=protected-access from pyomo.environ import Constraint diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py index 754c8d234a..135bf840c1 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py @@ -18,8 +18,8 @@ Flowsheet Optimization. Proceedings of the 13th International Symposium on Process Systems Engineering – PSE 2018, July 1-5, 2018, San Diego. """ -# TODO: Missing docstrings -# pylint: disable=missing-function-docstring +# TODO: Pylint complains about variables with _x names as they are built by other classes +# pylint: disable=protected-access from pyomo.environ import Constraint, Param, Var, value from idaes.core.util.exceptions import ConfigurationError diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index ee3ea3faa6..495ceb30bd 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -17,6 +17,8 @@ A complementarity-based vapor-liquid equilibrium formulation for equation-oriented simulation and optimization. AIChE Journal, DOI: 10.1002/aic.18029 """ +# TODO: Pylint complains about variables with _x names as they are built by other classes +# pylint: disable=protected-access from pyomo.environ import Constraint, Param, units as pyunits, Var, value From 851cc3bd3c13db3937db53d5d4dee94c44304ff0 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 18 Apr 2024 11:04:09 -0400 Subject: [PATCH 03/33] Fixing broken tests --- .../properties/modular_properties/eos/ceos.py | 34 ++++++++++--------- .../eos/tests/test_ceos_PR.py | 23 ++----------- .../examples/tests/test_BT_PR_SmoothVLE2.py | 2 +- 3 files changed, 22 insertions(+), 37 deletions(-) diff --git a/idaes/models/properties/modular_properties/eos/ceos.py b/idaes/models/properties/modular_properties/eos/ceos.py index 028a69e755..7b1666641c 100644 --- a/idaes/models/properties/modular_properties/eos/ceos.py +++ b/idaes/models/properties/modular_properties/eos/ceos.py @@ -449,24 +449,26 @@ def rule_delta_eq(m, p1, p2, p3, i): ) ) - b.add_component( - "_" + cname + "_delta_eq", - Expression(b.params._pe_pairs, b.phase_component_set, rule=rule_delta_eq), - ) - - def cubic_second_derivative_eq(b, p1, p2, p3): - b, _, _ = _calculate_equilibrium_cubic_coefficients( - b, cname, ctype, p1, p2, p3 + b.add_component( + "_" + cname + "_delta_eq", + Expression( + b.params._pe_pairs, b.phase_component_set, rule=rule_delta_eq + ), ) - z = b.compress_fact_phase[p3] - return 6 * z + 2 * b - b.add_component( - "_" + cname + "_cubic_second_derivative_eq", - Expression( - b.params._pe_pairs, b.phase_list, rule=cubic_second_derivative_eq - ), - ) + def cubic_second_derivative_eq(m, p1, p2, p3): + _b, _, _ = _calculate_equilibrium_cubic_coefficients( + m, cname, ctype, p1, p2, p3 + ) + z = m.compress_fact_phase[p3] + return 6 * z + 2 * _b + + b.add_component( + "_" + cname + "_cubic_second_derivative_eq", + Expression( + b.params._pe_pairs, b.phase_list, rule=cubic_second_derivative_eq + ), + ) @staticmethod def calculate_scaling_factors(b, pobj): diff --git a/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py b/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py index 0de49fe4c1..14ace32738 100644 --- a/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py +++ b/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py @@ -1089,8 +1089,8 @@ def test_calculate_equilibrium_cubic_coefficients(m): A_eq = m.props[1]._PR_A_eq["Vap", "Liq", "Liq"] B_eq = m.props[1]._PR_B_eq["Vap", "Liq", "Liq"] - EoS_u = EoS_param["PR"]["u"] - EoS_w = EoS_param["PR"]["w"] + EoS_u = EoS_param[CubicType.PR]["u"] + EoS_w = EoS_param[CubicType.PR]["w"] assert str(b) == str(-(1 + B_eq - EoS_u * B_eq)) assert str(c) == str(A_eq - EoS_u * B_eq - EoS_u * B_eq**2 + EoS_w * B_eq**2) @@ -1100,7 +1100,7 @@ def test_calculate_equilibrium_cubic_coefficients(m): @pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") @pytest.mark.unit def test_cubic_second_derivative(m): - EoS_u = EoS_param["PR"]["u"] + EoS_u = EoS_param[CubicType.PR]["u"] for k, v in m.props[1]._PR_cubic_second_derivative_eq.items(): B_eq = m.props[1]._PR_B_eq[k] @@ -1110,23 +1110,6 @@ def test_cubic_second_derivative(m): assert str(v) == str(6 * z + 2 * b) -@pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") -@pytest.mark.unit -def test_calculate_equilibrium_cubic_coefficients(m): - b, c, d = _calculate_equilibrium_cubic_coefficients( - m.props[1], "PR", CubicType.PR, "Vap", "Liq", "Liq" - ) - - A_eq = m.props[1]._PR_A_eq["Vap", "Liq", "Liq"] - B_eq = m.props[1]._PR_B_eq["Vap", "Liq", "Liq"] - EoS_u = EoS_param["PR"]["u"] - EoS_w = EoS_param["PR"]["w"] - - assert str(b) == str(-(1 + B_eq - EoS_u * B_eq)) - assert str(c) == str(A_eq - EoS_u * B_eq - EoS_u * B_eq**2 + EoS_w * B_eq**2) - assert str(d) == str(-(A_eq * B_eq + EoS_w * B_eq**2 + EoS_w * B_eq**3)) - - class TestCEOSCriticalProps: @pytest.fixture(scope="class") def model(self): diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py index a5374834fa..3822d217f9 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py @@ -48,7 +48,7 @@ SOUT = idaeslog.INFO # Set module level pyest marker -pytestmark = pytest.mark.cubic_root +pytestmark = [pytest.mark.cubic_root, pytest.mark.skip] # ----------------------------------------------------------------------------- From 251b7f723072d25e7aaa90e8e73cdc2ebdbf6655 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 18 Apr 2024 12:03:23 -0400 Subject: [PATCH 04/33] Fixing more tests --- .../modular_properties/base/utility.py | 32 ++++++++++++++----- .../eos/tests/test_ceos_PR.py | 2 +- .../phase_equil/smooth_VLE.py | 9 ------ .../phase_equil/tests/test_smooth_VLE.py | 9 +++--- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/idaes/models/properties/modular_properties/base/utility.py b/idaes/models/properties/modular_properties/base/utility.py index 960b31adbd..8788f78441 100644 --- a/idaes/models/properties/modular_properties/base/utility.py +++ b/idaes/models/properties/modular_properties/base/utility.py @@ -327,17 +327,33 @@ def identify_VL_component_list(blk, phase_pair): v_only_comps = [] pparams = blk.params - l_phase = None - v_phase = None + if pparams.get_phase(phase_pair[0]).is_liquid_phase(): l_phase = phase_pair[0] - elif pparams.get_phase(phase_pair[0]).is_vapor_phase(): - v_phase = phase_pair[0] - - if pparams.get_phase(phase_pair[1]).is_liquid_phase(): + if pparams.get_phase(phase_pair[1]).is_vapor_phase(): + v_phase = phase_pair[1] + else: + raise PropertyPackageError( + f"Phase pair {phase_pair[0]}-{phase_pair[1]} was identified as " + f"being a VLE pair, however {phase_pair[0]} is liquid but " + f"{phase_pair[1]} is not vapor." + ) + elif pparams.get_phase(phase_pair[1]).is_liquid_phase(): l_phase = phase_pair[1] - elif pparams.get_phase(phase_pair[1]).is_vapor_phase(): - v_phase = phase_pair[1] + if pparams.get_phase(phase_pair[0]).is_vapor_phase(): + v_phase = phase_pair[0] + else: + raise PropertyPackageError( + f"Phase pair {phase_pair[0]}-{phase_pair[1]} was identified as " + f"being a VLE pair, however {phase_pair[1]} is liquid but " + f"{phase_pair[0]} is not vapor." + ) + else: + raise PropertyPackageError( + f"Phase pair {phase_pair[0]}-{phase_pair[1]} was identified as " + f"being a VLE pair, however neither {phase_pair[0]} nor " + f"{phase_pair[1]} is liquid." + ) # Only need to do this for V-L pairs, so check if l_phase is not None and v_phase is not None: diff --git a/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py b/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py index 14ace32738..32eed0fcfd 100644 --- a/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py +++ b/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py @@ -1107,7 +1107,7 @@ def test_cubic_second_derivative(m): b = -(1 + B_eq - EoS_u * B_eq) z = m.props[1].compress_fact_phase[k[2]] - assert str(v) == str(6 * z + 2 * b) + assert str(v.expr) == str(6 * z + 2 * b) class TestCEOSCriticalProps: diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py index 135bf840c1..ce977c6544 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py @@ -22,7 +22,6 @@ # pylint: disable=protected-access from pyomo.environ import Constraint, Param, Var, value -from idaes.core.util.exceptions import ConfigurationError from idaes.core.util.math import smooth_max, smooth_min from idaes.models.properties.modular_properties.base.utility import ( identify_VL_component_list, @@ -50,14 +49,6 @@ def phase_equil(b, phase_pair): v_only_comps, ) = identify_VL_component_list(b, phase_pair) - if l_phase is None or v_phase is None: - raise ConfigurationError( - "{} Generic Property Package phase pair {}-{} was set to use " - "Smooth VLE formulation, however this is not a vapor-liquid pair.".format( - b.params.name, phase_pair[0], phase_pair[1] - ) - ) - # Definition of equilibrium temperature for smooth VLE t_units = b.params.get_metadata().default_units.TEMPERATURE if v_only_comps == []: diff --git a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE.py b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE.py index e8b1684af1..8d42935baf 100644 --- a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE.py +++ b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE.py @@ -33,7 +33,7 @@ from idaes.models.properties.modular_properties.state_definitions import FTPx from idaes.models.properties.modular_properties.phase_equil import SmoothVLE from idaes.models.properties.modular_properties.phase_equil.forms import fugacity -from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.exceptions import PropertyPackageError # Dummy EoS to use for fugacity calls @@ -166,9 +166,8 @@ def test_non_VLE_pair(): m.props = m.params.state_block_class([1], parameters=m.params) with pytest.raises( - ConfigurationError, - match="params Generic Property Package phase pair " - "Liq-Sol was set to use Smooth VLE formulation, " - "however this is not a vapor-liquid pair.", + PropertyPackageError, + match="Phase pair Liq-Sol was identified as being a VLE pair, however " + "Liq is liquid but Sol is not vapor.", ): SmoothVLE.phase_equil(m.props[1], ("Liq", "Sol")) From 7997adff84c34367aa0415564f44241f43a640ac Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 18 Apr 2024 12:50:21 -0400 Subject: [PATCH 05/33] Tests for identify_VL_component_list --- .../base/tests/test_utility.py | 157 +++++++++++++++++- .../modular_properties/base/utility.py | 41 ++--- 2 files changed, 170 insertions(+), 28 deletions(-) diff --git a/idaes/models/properties/modular_properties/base/tests/test_utility.py b/idaes/models/properties/modular_properties/base/tests/test_utility.py index c2b0034a7f..e1811fc318 100644 --- a/idaes/models/properties/modular_properties/base/tests/test_utility.py +++ b/idaes/models/properties/modular_properties/base/tests/test_utility.py @@ -16,9 +16,13 @@ Author: A Lee """ import pytest - from types import MethodType +from pyomo.environ import Block, ConcreteModel, units as pyunits, Var +from pyomo.common.config import ConfigBlock, ConfigValue + +from idaes.core import declare_process_block_class +from idaes.core import LiquidPhase, SolidPhase, VaporPhase, PhaseType as PT from idaes.models.properties.modular_properties.base.utility import ( GenericPropertyPackageError, get_method, @@ -27,13 +31,19 @@ get_bounds_from_config, get_concentration_term, ConcentrationForm, + identify_VL_component_list, +) +from idaes.models.properties.modular_properties.base.generic_property import ( + GenericParameterData, ) - from idaes.models.properties.modular_properties.base.generic_reaction import rxn_config -from pyomo.environ import Block, units as pyunits, Var -from pyomo.common.config import ConfigBlock, ConfigValue from idaes.core.util.exceptions import ConfigurationError, PropertyPackageError from idaes.core.util.misc import add_object_reference +from idaes.models.properties.modular_properties.base.tests.dummy_eos import DummyEoS +from idaes.models.properties.modular_properties.phase_equil.henry import ( + ConstantH, + HenryType, +) @pytest.fixture @@ -559,3 +569,142 @@ def test_inherent_partial_pressure(self, frame2): get_concentration_term(frame2, "i1", log=True) is frame2.log_pressure_phase_comp ) + + +class TestIdentifyVLComponentList: + # Declare a base units dict to save code later + base_units = { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + } + + # Dummy methods for properties + def set_metadata(self, b): + pass + + def define_state(self, b): + pass + + @declare_process_block_class("DummyParameterBlock") + class DummyParameterData(GenericParameterData): + def configure(self): + self.configured = True + + @pytest.fixture() + def frame(self): + m = ConcreteModel() + m.params = DummyParameterBlock( + components={ + "a": {}, + "b": {}, + "c": {}, + }, + phases={ + "p1": { + "type": LiquidPhase, + "equation_of_state": DummyEoS, + }, + "p2": { + "type": VaporPhase, + "equation_of_state": DummyEoS, + }, + "p3": { + "type": SolidPhase, + "equation_of_state": DummyEoS, + }, + }, + state_definition=self, + pressure_ref=100000.0, + temperature_ref=300, + base_units=self.base_units, + ) + + m.props = m.params.build_state_block([1], defined_state=False) + + return m + + @pytest.mark.unit + def test_invalid_VL_pair(self, frame): + with pytest.raises( + PropertyPackageError, + match="Phase pair p1-p3 was identified as being a VLE pair, " + "however p1 is liquid but p3 is not vapor.", + ): + identify_VL_component_list(frame.props[1], ("p1", "p3")) + + with pytest.raises( + PropertyPackageError, + match="Phase pair p2-p3 was identified as being a VLE pair, " + "however neither p2 nor p3 is liquid.", + ): + identify_VL_component_list(frame.props[1], ("p2", "p3")) + + @pytest.mark.unit + def test_all_components(self, frame): + l_phase, v_phase, vl_comps, henry_comps, l_only_comps, v_only_comps = ( + identify_VL_component_list(frame.props[1], ("p1", "p2")) + ) + + assert l_phase == "p1" + assert v_phase == "p2" + assert vl_comps == ["a", "b", "c"] + assert henry_comps == [] + assert l_only_comps == [] + assert v_only_comps == [] + + @pytest.mark.unit + def test_all_types_components(self): + m = ConcreteModel() + m.params = DummyParameterBlock( + components={ + "a": {}, + "b": { + "valid_phase_types": PT.liquidPhase, + }, + "c": { + "valid_phase_types": PT.vaporPhase, + }, + "d": { + "valid_phase_types": PT.solidPhase, + }, + "e": { + "parameter_data": {"henry_ref": {"p1": 86}}, + "henry_component": { + "p1": {"method": ConstantH, "type": HenryType.Kpx} + }, + }, + }, + phases={ + "p1": { + "type": LiquidPhase, + "equation_of_state": DummyEoS, + }, + "p2": { + "type": VaporPhase, + "equation_of_state": DummyEoS, + }, + "p3": { + "type": SolidPhase, + "equation_of_state": DummyEoS, + }, + }, + state_definition=self, + pressure_ref=100000.0, + temperature_ref=300, + base_units=self.base_units, + ) + + m.props = m.params.build_state_block([1], defined_state=False) + + l_phase, v_phase, vl_comps, henry_comps, l_only_comps, v_only_comps = ( + identify_VL_component_list(m.props[1], ("p1", "p2")) + ) + assert l_phase == "p1" + assert v_phase == "p2" + assert vl_comps == ["a"] + assert henry_comps == ["e"] + assert l_only_comps == ["b"] + assert v_only_comps == ["c"] diff --git a/idaes/models/properties/modular_properties/base/utility.py b/idaes/models/properties/modular_properties/base/utility.py index 8788f78441..953392f055 100644 --- a/idaes/models/properties/modular_properties/base/utility.py +++ b/idaes/models/properties/modular_properties/base/utility.py @@ -355,30 +355,23 @@ def identify_VL_component_list(blk, phase_pair): f"{phase_pair[1]} is liquid." ) - # Only need to do this for V-L pairs, so check - if l_phase is not None and v_phase is not None: - for j in blk.params.component_list: - if (l_phase, j) in blk.phase_component_set and ( - v_phase, - j, - ) in blk.phase_component_set: - cobj = pparams.get_component(j) - if cobj.config.henry_component is not None and ( - phase_pair[0] in cobj.config.henry_component - or phase_pair[1] in cobj.config.henry_component - ): - henry_comps.append(j) - else: - vl_comps.append(j) - elif (l_phase, j) in blk.phase_component_set: - l_only_comps.append(j) - elif (v_phase, j) in blk.phase_component_set: - v_only_comps.append(j) - else: - vl_comps = [] - henry_comps = [] - l_only_comps = [] - v_only_comps = [] + for j in blk.params.component_list: + if (l_phase, j) in blk.phase_component_set and ( + v_phase, + j, + ) in blk.phase_component_set: + cobj = pparams.get_component(j) + if cobj.config.henry_component is not None and ( + phase_pair[0] in cobj.config.henry_component + or phase_pair[1] in cobj.config.henry_component + ): + henry_comps.append(j) + else: + vl_comps.append(j) + elif (l_phase, j) in blk.phase_component_set: + l_only_comps.append(j) + elif (v_phase, j) in blk.phase_component_set: + v_only_comps.append(j) return l_phase, v_phase, vl_comps, henry_comps, l_only_comps, v_only_comps From 83539aa7b88ec24a59a58bca1c22ebe9d14dfc12 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Sun, 21 Apr 2024 16:01:18 -0400 Subject: [PATCH 06/33] Testing estimation of bubble and dew points --- .../base/generic_property.py | 9 +- .../base/tests/test_generic_property.py | 53 ++++++++ .../base/tests/test_utility.py | 125 +++++++++++++++++- 3 files changed, 180 insertions(+), 7 deletions(-) diff --git a/idaes/models/properties/modular_properties/base/generic_property.py b/idaes/models/properties/modular_properties/base/generic_property.py index 4815e39643..f7523b2027 100644 --- a/idaes/models/properties/modular_properties/base/generic_property.py +++ b/idaes/models/properties/modular_properties/base/generic_property.py @@ -883,13 +883,10 @@ def build(self): if build_parameters is not None: try: build_parameters(pobj) - except KeyError: + except KeyError as err: raise ConfigurationError( - "{} values were not defined for parameter {} in " - "phase {}. Please check the parameter_data " - "argument to ensure values are provided.".format( - self.name, a, p - ) + f"{self.name} - values were not defined for parameter {a} in " + f"phase {p}. {str(err)}" ) # Next, add inherent reactions if they exist diff --git a/idaes/models/properties/modular_properties/base/tests/test_generic_property.py b/idaes/models/properties/modular_properties/base/tests/test_generic_property.py index 8a99e2fb47..04ac4cad59 100644 --- a/idaes/models/properties/modular_properties/base/tests/test_generic_property.py +++ b/idaes/models/properties/modular_properties/base/tests/test_generic_property.py @@ -42,6 +42,8 @@ ) from idaes.core.util.exceptions import ConfigurationError, PropertyPackageError from idaes.models.properties.modular_properties.phase_equil.henry import HenryType +from idaes.models.properties.modular_properties.eos.ceos import Cubic, CubicType +from idaes.models.properties.modular_properties.state_definitions import FTPx from idaes.core.base.property_meta import UnitSet from idaes.core.initialization import BlockTriangularizationInitializer @@ -1955,3 +1957,54 @@ def build_critical_properties(b, *args, **kwargs): "Component declarations.", ): _initialize_critical_props(m.props[1]) + + +# Invalid property configuration to trigger configuration error +configuration = { + # Specifying components + "components": { + "H2O": { + "type": Component, + "parameter_data": { + "pressure_crit": (220.6e5, pyunits.Pa), + "temperature_crit": (647, pyunits.K), + "omega": 0.344, + }, + }, + }, + # Specifying phases + "phases": { + "Liq": { + "type": LiquidPhase, + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + "Vap": { + "type": VaporPhase, + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + }, + # Set base units of measurement + "base_units": { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + }, + # Specifying state definition + "state_definition": FTPx, + "state_bounds": { + "flow_mol": (0, 100, 1000, pyunits.mol / pyunits.s), + "temperature": (273.15, 300, 500, pyunits.K), + "pressure": (5e4, 1e5, 1e6, pyunits.Pa), + }, + "pressure_ref": (101325, pyunits.Pa), + "temperature_ref": (298.15, pyunits.K), + "parameter_data": { + "PR_kappa": { + ("foo", "bar"): 0.000, + } + }, +} diff --git a/idaes/models/properties/modular_properties/base/tests/test_utility.py b/idaes/models/properties/modular_properties/base/tests/test_utility.py index e1811fc318..0bf11fd926 100644 --- a/idaes/models/properties/modular_properties/base/tests/test_utility.py +++ b/idaes/models/properties/modular_properties/base/tests/test_utility.py @@ -22,7 +22,7 @@ from pyomo.common.config import ConfigBlock, ConfigValue from idaes.core import declare_process_block_class -from idaes.core import LiquidPhase, SolidPhase, VaporPhase, PhaseType as PT +from idaes.core import Component, LiquidPhase, SolidPhase, VaporPhase, PhaseType as PT from idaes.models.properties.modular_properties.base.utility import ( GenericPropertyPackageError, get_method, @@ -32,6 +32,10 @@ get_concentration_term, ConcentrationForm, identify_VL_component_list, + estimate_Pbub, + estimate_Pdew, + estimate_Tbub, + estimate_Tdew, ) from idaes.models.properties.modular_properties.base.generic_property import ( GenericParameterData, @@ -44,6 +48,14 @@ ConstantH, HenryType, ) +from idaes.models.properties.modular_properties.eos.ceos import Cubic, CubicType +from idaes.models.properties.modular_properties.phase_equil import SmoothVLE +from idaes.models.properties.modular_properties.phase_equil.bubble_dew import ( + LogBubbleDew, +) +from idaes.models.properties.modular_properties.phase_equil.forms import log_fugacity +from idaes.models.properties.modular_properties.pure import NIST +from idaes.models.properties.modular_properties.state_definitions import FTPx @pytest.fixture @@ -708,3 +720,114 @@ def test_all_types_components(self): assert henry_comps == ["e"] assert l_only_comps == ["b"] assert v_only_comps == ["c"] + + +# Property configuration for pure water to use in bubble and dew point tests +configuration = { + # Specifying components + "components": { + "H2O": { + "type": Component, + "pressure_sat_comp": NIST, + "phase_equilibrium_form": {("Vap", "Liq"): log_fugacity}, + "parameter_data": { + "pressure_crit": (220.6e5, pyunits.Pa), + "temperature_crit": (647, pyunits.K), + "omega": 0.344, + "pressure_sat_comp_coeff": { + "A": (3.55959, None), + "B": (643.748, pyunits.K), + "C": (-198.043, pyunits.K), + }, + }, + }, + }, + # Specifying phases + "phases": { + "Liq": { + "type": LiquidPhase, + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + "Vap": { + "type": VaporPhase, + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + }, + # Set base units of measurement + "base_units": { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + }, + # Specifying state definition + "state_definition": FTPx, + "state_bounds": { + "flow_mol": (0, 100, 1000, pyunits.mol / pyunits.s), + "temperature": (273.15, 300, 500, pyunits.K), + "pressure": (5e4, 1e5, 1e6, pyunits.Pa), + }, + "pressure_ref": (101325, pyunits.Pa), + "temperature_ref": (298.15, pyunits.K), + # Defining phase equilibria + "phases_in_equilibrium": [("Vap", "Liq")], + "phase_equilibrium_state": {("Vap", "Liq"): SmoothVLE}, + "bubble_dew_method": LogBubbleDew, + "parameter_data": { + "PR_kappa": { + ("H2O", "H2O"): 0.000, + } + }, +} + + +class TestBubbleDewPoints: + @pytest.fixture + def model(self): + m = ConcreteModel() + m.params = DummyParameterBlock( + **configuration, + ) + + m.props = m.params.build_state_block([1], defined_state=False) + + return m + + @pytest.mark.unit + def test_bubble_temperature(self, model): + # Test bubble temperature at atmospheric pressure + model.props[1].pressure.set_value(101325) + Tbub = estimate_Tbub(model.props[1], pyunits.K, ["H2O"], [], "Liq") + + # Expected value = 379.1828 from parameters used + assert Tbub == pytest.approx(379.1828, rel=1e-6) + + @pytest.mark.unit + def test_dew_temperature(self, model): + # Test dew temperature at atmospheric pressure + model.props[1].pressure.set_value(101325) + Tdew = estimate_Tdew(model.props[1], pyunits.K, ["H2O"], [], "Liq") + + # Expected value = 379.1828 from parameters used + assert Tdew == pytest.approx(379.1828, rel=1e-6) + + @pytest.mark.unit + def test_bubble_pressure(self, model): + # Test bubble pressure at 100C + model.props[1].temperature.set_value(373.15) + Pbub = estimate_Pbub(model.props[1], ["H2O"], [], "Liq") + + # Expected value = 76432.45 from parameters used + assert Pbub == pytest.approx(76432.45, rel=1e-6) + + @pytest.mark.unit + def test_dew_pressure(self, model): + # Test dew pressure at 100C + model.props[1].temperature.set_value(373.15) + Pdew = estimate_Pdew(model.props[1], ["H2O"], [], "Liq") + + # Expected value = 76432.45 from parameters used + assert Pdew == pytest.approx(76432.45, rel=1e-6) From 59e22dd92603c0cc81fa1de15e7874fc9143c78a Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 22 Apr 2024 15:10:08 -0400 Subject: [PATCH 07/33] First unit testing of SmoothVLE2 --- .../base/generic_property.py | 6 +- .../properties/modular_properties/eos/ceos.py | 28 +-- .../eos/tests/test_ceos_PR.py | 17 +- .../examples/tests/test_BT_PR_SmoothVLE2.py | 5 +- .../phase_equil/smooth_VLE.py | 10 +- .../phase_equil/smooth_VLE_2.py | 153 ++++++------ .../phase_equil/tests/test_smooth_VLE_2.py | 231 ++++++++++++++++++ 7 files changed, 331 insertions(+), 119 deletions(-) create mode 100644 idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py diff --git a/idaes/models/properties/modular_properties/base/generic_property.py b/idaes/models/properties/modular_properties/base/generic_property.py index f7523b2027..2af3500637 100644 --- a/idaes/models/properties/modular_properties/base/generic_property.py +++ b/idaes/models/properties/modular_properties/base/generic_property.py @@ -2097,9 +2097,13 @@ def build(self): ): t_units = self.params.get_metadata().default_units.TEMPERATURE + if self.temperature.value is not None: + t_value = value(self.temperature) + else: + t_value = None self._teq = Var( self.params._pe_pairs, - initialize=value(self.temperature), + initialize=t_value, doc="Temperature for calculating phase equilibrium", units=t_units, ) diff --git a/idaes/models/properties/modular_properties/eos/ceos.py b/idaes/models/properties/modular_properties/eos/ceos.py index 7b1666641c..2be582d984 100644 --- a/idaes/models/properties/modular_properties/eos/ceos.py +++ b/idaes/models/properties/modular_properties/eos/ceos.py @@ -456,20 +456,6 @@ def rule_delta_eq(m, p1, p2, p3, i): ), ) - def cubic_second_derivative_eq(m, p1, p2, p3): - _b, _, _ = _calculate_equilibrium_cubic_coefficients( - m, cname, ctype, p1, p2, p3 - ) - z = m.compress_fact_phase[p3] - return 6 * z + 2 * _b - - b.add_component( - "_" + cname + "_cubic_second_derivative_eq", - Expression( - b.params._pe_pairs, b.phase_list, rule=cubic_second_derivative_eq - ), - ) - @staticmethod def calculate_scaling_factors(b, pobj): pass @@ -1366,7 +1352,7 @@ def a(k): # ----------------------------------------------------------------------------- # Default rules for cubic expressions -def _calculate_equilibrium_cubic_coefficients(b, cubic_name, cubic_type, p1, p2, p3): +def calculate_equilibrium_cubic_coefficients(b, cubic_name, cubic_type, p1, p2, p3): """ Calculates the coefficients b, c, and d of the cubic 0 = z**3 + b * z**2 + c * z + d at the equilibrium conditions @@ -1383,10 +1369,8 @@ def _calculate_equilibrium_cubic_coefficients(b, cubic_name, cubic_type, p1, p2, expressions for b, c, d """ - A_eq = getattr(b, "_" + cubic_name + "_A_eq") - B_eq = getattr(b, "_" + cubic_name + "_B_eq") - A_eq = A_eq[p1, p2, p3] - B_eq = B_eq[p1, p2, p3] + A_eq = getattr(b, "_" + cubic_name + "_A_eq")[p1, p2, p3] + B_eq = getattr(b, "_" + cubic_name + "_B_eq")[p1, p2, p3] EoS_u = EoS_param[cubic_type]["u"] EoS_w = EoS_param[cubic_type]["w"] @@ -1427,7 +1411,7 @@ def func_fw_SRK(cobj): def func_alpha_soave(T, fw, cobj): """ - alpha function for SRK EoS. + Soave alpha function. Args: fw: expression for fw @@ -1444,7 +1428,7 @@ def func_alpha_soave(T, fw, cobj): def func_dalpha_dT_soave(T, fw, cobj): """ - Function to get first derivative of alpha for SRK EoS. + Function to get first derivative of Soave alpha function. Args: fw: expression for fw @@ -1461,7 +1445,7 @@ def func_dalpha_dT_soave(T, fw, cobj): def func_d2alpha_dT2_soave(T, fw, cobj): """ - Function to get 2nd derivative of alpha for SRK EoS. + Function to get 2nd derivative of Soave alpha function. Args: fw: expression for fw diff --git a/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py b/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py index 32eed0fcfd..6509009d44 100644 --- a/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py +++ b/idaes/models/properties/modular_properties/eos/tests/test_ceos_PR.py @@ -43,7 +43,7 @@ from idaes.core.util.constants import Constants as const from idaes.models.properties.modular_properties.eos.ceos import ( cubic_roots_available, - _calculate_equilibrium_cubic_coefficients, + calculate_equilibrium_cubic_coefficients, EoS_param, ) from idaes.core.solvers import get_solver @@ -1083,7 +1083,7 @@ def test_vol_mol_phase_comp(m): @pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") @pytest.mark.unit def test_calculate_equilibrium_cubic_coefficients(m): - b, c, d = _calculate_equilibrium_cubic_coefficients( + b, c, d = calculate_equilibrium_cubic_coefficients( m.props[1], "PR", CubicType.PR, "Vap", "Liq", "Liq" ) @@ -1097,19 +1097,6 @@ def test_calculate_equilibrium_cubic_coefficients(m): assert str(d) == str(-(A_eq * B_eq + EoS_w * B_eq**2 + EoS_w * B_eq**3)) -@pytest.mark.skipif(not cubic_roots_available(), reason="Cubic functions not available") -@pytest.mark.unit -def test_cubic_second_derivative(m): - EoS_u = EoS_param[CubicType.PR]["u"] - - for k, v in m.props[1]._PR_cubic_second_derivative_eq.items(): - B_eq = m.props[1]._PR_B_eq[k] - b = -(1 + B_eq - EoS_u * B_eq) - z = m.props[1].compress_fact_phase[k[2]] - - assert str(v.expr) == str(6 * z + 2 * b) - - class TestCEOSCriticalProps: @pytest.fixture(scope="class") def model(self): diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py index 3822d217f9..0f2659f85a 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py @@ -48,7 +48,7 @@ SOUT = idaeslog.INFO # Set module level pyest marker -pytestmark = [pytest.mark.cubic_root, pytest.mark.skip] +pytestmark = pytest.mark.cubic_root # ----------------------------------------------------------------------------- @@ -196,10 +196,11 @@ def m(self): m.fs.props = GenericParameterBlock(**configuration) - m.fs.state = m.fs.props.build_state_block([1], defined_state=True) + m.fs.state = m.fs.props.build_state_block([1], defined_state=False) iscale.calculate_scaling_factors(m.fs.props) iscale.calculate_scaling_factors(m.fs.state[1]) + return m @pytest.mark.integration diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py index ce977c6544..d51d411c30 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py @@ -145,15 +145,15 @@ def calculate_teq(b, phase_pair): if hasattr(b, "eq_temperature_bubble"): _t1 = getattr(b, "_t1" + suffix) - _t1.value = max( - value(b.temperature), b.temperature_bubble[phase_pair].value + _t1.set_value( + max(value(b.temperature), value(b.temperature_bubble[phase_pair])) ) else: _t1 = b.temperature if hasattr(b, "eq_temperature_dew"): - b._teq[phase_pair].value = min( - _t1.value, b.temperature_dew[phase_pair].value + b._teq[phase_pair].set_value( + min(value(_t1), value(b.temperature_dew[phase_pair])) ) else: - b._teq[phase_pair].value = _t1.value + b._teq[phase_pair].set_value(value(_t1)) diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 495ceb30bd..fda1ad7c60 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -20,7 +20,7 @@ # TODO: Pylint complains about variables with _x names as they are built by other classes # pylint: disable=protected-access -from pyomo.environ import Constraint, Param, units as pyunits, Var, value +from pyomo.environ import Constraint, Expression, Param, units as pyunits, Var, value from idaes.core.util.exceptions import ConfigurationError from idaes.core.util.math import smooth_min @@ -31,6 +31,9 @@ estimate_Tbub, estimate_Tdew, ) +from idaes.models.properties.modular_properties.eos.ceos import ( + calculate_equilibrium_cubic_coefficients, +) import idaes.core.util.scaling as iscale @@ -62,11 +65,16 @@ def phase_equil(b, phase_pair): "was set to use Smooth VLE formulation, however this is not a vapor-liquid pair." ) + lobj = b.params.get_phase(l_phase) + ctype = lobj._cubic_type + cname = lobj.config.equation_of_state_options["type"].name + # Definition of equilibrium temperature for smooth VLE uom = b.params.get_metadata().default_units t_units = uom.TEMPERATURE f_units = uom.AMOUNT / uom.TIME + # TODO: Do these need to be indexed by ALL phases, or just the VLE phases? s = Var( b.params.phase_list, initialize=0.0, @@ -92,7 +100,7 @@ def rule_teq(b): == 0 ) - b.add_component("_tbar_constraint" + suffix, Constraint(rule=rule_teq)) + b.add_component("_teq_constraint" + suffix, Constraint(rule=rule_teq)) eps = Param( default=1e-04, @@ -133,15 +141,24 @@ def rule_temperature_slack_complementarity(b, p): ), ) - def rule_cubic_root_complementarity(b, p): + def rule_cubic_second_derivative(b, p): p1, p2 = phase_pair - pobj = b.params.get_phase(p) - cname = pobj.config.equation_of_state_options["type"].name - cubic_second_derivative = getattr( - b, - "_" + cname + "_cubic_second_derivative", + + _b, _, _ = calculate_equilibrium_cubic_coefficients( + b, cname, ctype, p1, p2, p ) - return cubic_second_derivative[p1, p2, p] == gp[p] - gn[p] + z = b.compress_fact_phase[p] + + return 6 * z + 2 * _b + + b.add_component( + "cubic_second_derivative" + suffix, + Expression(b.params.phase_list, rule=rule_cubic_second_derivative), + ) + + def rule_cubic_root_complementarity(b, p): + der = getattr(b, "cubic_second_derivative" + suffix) + return der[p] == gp[p] - gn[p] b.add_component( "cubic_root_complementarity" + suffix, @@ -179,38 +196,46 @@ def calculate_teq(blk, pp): # equilibrium temperature _teq T_units = blk.params.get_metadata().default_units.TEMPERATURE - liquid_phase, _, raoult_comps, henry_comps, _, _ = identify_VL_component_list( - blk, pp - ) - - Tbub = estimate_Tbub(blk, T_units, raoult_comps, henry_comps, liquid_phase) - Tdew = estimate_Tbub(blk, T_units, raoult_comps, henry_comps, liquid_phase) - - assert False - suffix = "_" + phase_pair[0] + "_" + phase_pair[1] + ( + liquid_phase, + vapor_phase, + raoult_comps, + henry_comps, + l_only_comps, + v_only_comps, + ) = identify_VL_component_list(blk, pp) - if hasattr(b, "eq_temperature_bubble"): - _t1 = getattr(b, "_t1" + suffix) - _t1.value = max( - value(b.temperature), b.temperature_bubble[phase_pair].value - ) + if v_only_comps is None: + if blk.is_property_constructed("temperature_bubble"): + Tbub = value(blk.temeprature_bubble[pp]) + else: + Tbub = estimate_Tbub( + blk, T_units, raoult_comps, henry_comps, liquid_phase + ) + t1 = max(value(blk.temperature), Tbub) else: - _t1 = b.temperature + t1 = value(blk.temperature) - if hasattr(b, "eq_temperature_dew"): - b._teq[phase_pair].value = min( - _t1.value, b.temperature_dew[phase_pair].value - ) + if v_only_comps is None: + if blk.is_property_constructed("temperature_dew"): + Tdew = value(blk.temeprature_bubble[pp]) + else: + Tdew = estimate_Tdew( + blk, T_units, raoult_comps, henry_comps, liquid_phase + ) + t2 = min(t1, Tdew) else: - b._teq[phase_pair].value = _t1.value + t2 = t1 + + blk._teq[pp].set_value(t2) # --------------------------------------------------------------------- # Initialize sV and sL slacks - _calculate_temperature_slacks(blk, pp) + _calculate_temperature_slacks(blk, pp, liquid_phase, vapor_phase) # --------------------------------------------------------------------- # If flash, initialize g+ and g- slacks - _calculate_ceos_derivative_slacks(blk, pp) + _calculate_ceos_derivative_slacks(blk, liquid_phase, vapor_phase) @staticmethod def phase_equil_initialization(b, phase_pair): @@ -222,59 +247,39 @@ def phase_equil_initialization(b, phase_pair): c.deactivate() -def _calculate_temperature_slacks(b, phase_pair): +def _calculate_temperature_slacks(b, phase_pair, liquid_phase, vapor_phase): suffix = "_" + phase_pair[0] + "_" + phase_pair[1] s = getattr(b, "s" + suffix) - if b.params.get_phase(phase_pair[0]).is_vapor_phase(): - vapor_phase = phase_pair[0] - liquid_phase = phase_pair[1] - else: - vapor_phase = phase_pair[1] - liquid_phase = phase_pair[0] - - if b._teq[phase_pair].value > b.temperature.value: - s[vapor_phase].value = value(b._teq[phase_pair] - b.temperature) - s[liquid_phase].value = 0 - elif b._teq[phase_pair].value < b.temperature.value: - s[vapor_phase].value = 0 - s[liquid_phase].value = value(b.temperature - b._teq[phase_pair]) + if value(b._teq[phase_pair]) > value(b.temperature): + s[vapor_phase].set_value(value(b._teq[phase_pair] - b.temperature)) + s[liquid_phase].set_value(0) + elif value(b._teq[phase_pair]) < value(b.temperature): + s[vapor_phase].set_value(0) + s[liquid_phase].set_value(value(b.temperature - b._teq[phase_pair])) else: - s[vapor_phase].value = 0 - s[liquid_phase].value = 0 + s[vapor_phase].set_value(0) + s[liquid_phase].set_value(0) -def _calculate_ceos_derivative_slacks(b, phase_pair): +def _calculate_ceos_derivative_slacks(b, phase_pair, liquid_phase, vapor_phase): suffix = "_" + phase_pair[0] + "_" + phase_pair[1] - p1, p2 = phase_pair gp = getattr(b, "gp" + suffix) gn = getattr(b, "gn" + suffix) + der = getattr(b, "cubic_second_derivative" + suffix) - if b.params.get_phase(phase_pair[0]).is_vapor_phase(): - vapor_phase = phase_pair[0] - liquid_phase = phase_pair[1] + if value(der[liquid_phase]) < 0: + gp[liquid_phase].set_value(0) + gn[liquid_phase].set_value(value(-der[liquid_phase])) else: - vapor_phase = phase_pair[1] - liquid_phase = phase_pair[0] - - vapobj = b.params.get_phase(vapor_phase) - liqobj = b.params.get_phase(liquid_phase) - cname_vap = vapobj.config.equation_of_state_options["type"].name - cname_liq = liqobj.config.equation_of_state_options["type"].name - cubic_second_derivative_vap = getattr( - b, "_" + cname_vap + "_cubic_second_derivative" - ) - cubic_second_derivative_liq = getattr( - b, "_" + cname_liq + "_cubic_second_derivative" - ) - - if value(cubic_second_derivative_liq[p1, p2, liquid_phase]) < 0: - gp[liquid_phase].value = 0 - gn[liquid_phase].value = value( - -cubic_second_derivative_liq[p1, p2, liquid_phase] - ) - if value(cubic_second_derivative_vap[p1, p2, vapor_phase]) > 0: - gp[vapor_phase].value = value(cubic_second_derivative_vap[p1, p2, vapor_phase]) - gn[vapor_phase].value = 0 + gp[liquid_phase].set_value(0) + gn[liquid_phase].set_value(0) + + if value(der[vapor_phase]) > 0: + gp[vapor_phase].set_value(value(der[vapor_phase])) + gn[vapor_phase].set_value(0) + else: + gp[vapor_phase].set_value(0) + gn[vapor_phase].set_value(0) diff --git a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py new file mode 100644 index 0000000000..23243e157e --- /dev/null +++ b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py @@ -0,0 +1,231 @@ +################################################################################# +# 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-2023 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. +################################################################################# +""" +Tests for SmoothVLE2 formulation + +Authors: Andrew Lee +""" + +import pytest + +from pyomo.environ import ( + ConcreteModel, + Constraint, + Expression, + Param, + value, + Var, + units as pyunits, +) + +from idaes.models.properties.modular_properties.base.generic_property import ( + GenericParameterBlock, +) +from idaes.models.properties.modular_properties.state_definitions import FTPx +from idaes.models.properties.modular_properties.phase_equil.smooth_VLE_2 import ( + SmoothVLE2, + _calculate_temperature_slacks, + _calculate_ceos_derivative_slacks, +) +from idaes.models.properties.modular_properties.eos.ceos import Cubic, CubicType +from idaes.models.properties.modular_properties.phase_equil.forms import fugacity + + +@pytest.fixture() +def frame(): + m = ConcreteModel() + + # Create a dummy parameter block + m.params = GenericParameterBlock( + components={ + "H2O": { + "parameter_data": { + "pressure_crit": (220.6e5, pyunits.Pa), + "temperature_crit": (647, pyunits.K), + "omega": 0.344, + }, + "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, + } + }, + phases={ + "Liq": { + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + "Vap": { + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + }, + state_definition=FTPx, + pressure_ref=100000.0, + temperature_ref=300, + base_units={ + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + }, + phases_in_equilibrium=[("Vap", "Liq")], + phase_equilibrium_state={("Vap", "Liq"): SmoothVLE2}, + parameter_data={ + "PR_kappa": { + ("H2O", "H2O"): 0.000, + } + }, + ) + + # Create a dummy state block + m.props = m.params.state_block_class([1], parameters=m.params) + + return m + + +@pytest.mark.unit +def test_build(frame): + assert isinstance(frame.props[1].s_Vap_Liq, Var) + assert isinstance(frame.props[1].gp_Vap_Liq, Var) + assert isinstance(frame.props[1].gn_Vap_Liq, Var) + + assert isinstance(frame.props[1].cubic_second_derivative_Vap_Liq, Expression) + + assert isinstance(frame.props[1].eps_Vap_Liq, Param) + assert value(frame.props[1].eps_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) + + assert isinstance(frame.props[1]._teq_constraint_Vap_Liq, Constraint) + assert isinstance( + frame.props[1].temperature_slack_complementarity_Vap_Liq, Constraint + ) + assert isinstance(frame.props[1].cubic_slack_complementarity_Vap_Liq, Constraint) + + for k in ["Liq", "Vap"]: + assert k in frame.props[1].s_Vap_Liq + assert k in frame.props[1].gp_Vap_Liq + assert k in frame.props[1].gn_Vap_Liq + assert k in frame.props[1].cubic_second_derivative_Vap_Liq + assert k in frame.props[1].temperature_slack_complementarity_Vap_Liq + assert k in frame.props[1].cubic_slack_complementarity_Vap_Liq + + +# TODO: Need tests for formulation + + +@pytest.mark.unit +def test_cubic_second_derivative(frame): + Z = frame.props[1].compress_fact_phase + + # For pure water + R = 8.314 + b = 0.07780 * R * 647 / 220.6e5 + + for P in range(1, 11): + for T in range(300, 500, 10): + frame.props[1].pressure.set_value(P * 1e5) + frame.props[1].temperature.set_value(T) + frame.props[1]._teq[("Vap", "Liq")].set_value(T) + + B = b * P * 1e5 / R / T + + for p in ["Vap", "Liq"]: + der = value(6 * Z[p] + 2 * -(1 + B - 2 * B)) + assert value( + frame.props[1].cubic_second_derivative_Vap_Liq[p] + ) == pytest.approx(der, rel=1e-8) + + +@pytest.mark.unit +def test_calculate_temperature_slacks(frame): + s = frame.props[1].s_Vap_Liq + # Teq > T + frame.props[1].temperature.set_value(300) + frame.props[1]._teq[("Vap", "Liq")].set_value(400) + _calculate_temperature_slacks(frame.props[1], ("Vap", "Liq"), "Liq", "Vap") + + assert value(s["Vap"]) == pytest.approx(100, rel=1e-8) + assert value(s["Liq"]) == pytest.approx(0, abs=1e-8) + + # Teq < T + frame.props[1]._teq[("Vap", "Liq")].set_value(200) + _calculate_temperature_slacks(frame.props[1], ("Vap", "Liq"), "Liq", "Vap") + + assert value(s["Liq"]) == pytest.approx(100, rel=1e-8) + assert value(s["Vap"]) == pytest.approx(0, abs=1e-8) + + # Teq == T + frame.props[1]._teq[("Vap", "Liq")].set_value(300) + _calculate_temperature_slacks(frame.props[1], ("Vap", "Liq"), "Liq", "Vap") + + assert value(s["Liq"]) == pytest.approx(0, abs=1e-8) + assert value(s["Vap"]) == pytest.approx(0, abs=1e-8) + + +@pytest.mark.unit +def test_calculate_ceos_derivative_slacks(frame): + gp = frame.props[1].gp_Vap_Liq + gn = frame.props[1].gn_Vap_Liq + der = frame.props[1].cubic_second_derivative_Vap_Liq + Z = frame.props[1].compress_fact_phase + + # For pure water + R = 8.314 + b = 0.07780 * R * 647 / 220.6e5 + + # Atmospheric conditions, vapor derivative +ve, liquid -ve + frame.props[1].pressure.set_value(1e5) + frame.props[1].temperature.set_value(300) + frame.props[1]._teq[("Vap", "Liq")].set_value(300) + + _calculate_ceos_derivative_slacks(frame.props[1], ("Vap", "Liq"), "Liq", "Vap") + + B = value(b * frame.props[1].pressure / R / frame.props[1].temperature) + der_l = value(6 * Z["Liq"] + 2 * -(1 + B - 2 * B)) + der_v = value(6 * Z["Vap"] + 2 * -(1 + B - 2 * B)) + + assert value(gp["Liq"]) == pytest.approx(0, abs=1e-8) + assert value(gn["Liq"]) == pytest.approx(-der_l, rel=1e-8) + + assert value(gp["Vap"]) == pytest.approx(der_v, rel=1e-8) + assert value(gn["Vap"]) == pytest.approx(0, abs=1e-8) + + # Supercritical conditions, vapor derivative +ve, liquid +ve + frame.props[1].pressure.set_value(250e5) + frame.props[1].temperature.set_value(700) + frame.props[1]._teq[("Vap", "Liq")].set_value(700) + + _calculate_ceos_derivative_slacks(frame.props[1], ("Vap", "Liq"), "Liq", "Vap") + + B = value(b * frame.props[1].pressure / R / frame.props[1].temperature) + der_v = value(6 * Z["Vap"] + 2 * -(1 + B - 2 * B)) + + assert value(gp["Liq"]) == pytest.approx(0, abs=1e-8) + assert value(gn["Liq"]) == pytest.approx(0, abs=1e-8) + + assert value(gp["Vap"]) == pytest.approx(der_v, rel=1e-8) + assert value(gn["Vap"]) == pytest.approx(0, abs=1e-8) + + # Supercritical conditions, vapor derivative -ve, liquid -ve + frame.props[1].pressure.set_value(250e5) + frame.props[1].temperature.set_value(300) + frame.props[1]._teq[("Vap", "Liq")].set_value(300) + + _calculate_ceos_derivative_slacks(frame.props[1], ("Vap", "Liq"), "Liq", "Vap") + + B = value(b * frame.props[1].pressure / R / frame.props[1].temperature) + der_l = value(6 * Z["Liq"] + 2 * -(1 + B - 2 * B)) + + assert value(gp["Liq"]) == pytest.approx(0, abs=1e-8) + assert value(gn["Liq"]) == pytest.approx(-der_l, rel=1e-8) + + assert value(gp["Vap"]) == pytest.approx(0, abs=1e-8) + assert value(gn["Vap"]) == pytest.approx(0, abs=1e-8) From d76b17decc3cac0f4bb116fdf2875f21472efdde Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 23 Apr 2024 09:27:46 -0400 Subject: [PATCH 08/33] First running version of code --- .../properties/modular_properties/phase_equil/smooth_VLE_2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index fda1ad7c60..2272a4ae64 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -235,7 +235,7 @@ def calculate_teq(blk, pp): # --------------------------------------------------------------------- # If flash, initialize g+ and g- slacks - _calculate_ceos_derivative_slacks(blk, liquid_phase, vapor_phase) + _calculate_ceos_derivative_slacks(blk, pp, liquid_phase, vapor_phase) @staticmethod def phase_equil_initialization(b, phase_pair): From 3555f5847cd99724bc669b9c5a6f287f0ba821e4 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 23 Apr 2024 14:20:14 -0400 Subject: [PATCH 09/33] Running example --- .../examples/tests/test_BT_PR_SmoothVLE2.py | 114 ++---------------- .../phase_equil/smooth_VLE_2.py | 20 +-- 2 files changed, 20 insertions(+), 114 deletions(-) diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py index 0f2659f85a..0b6bd086d5 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py @@ -45,7 +45,7 @@ import idaes.logger as idaeslog -SOUT = idaeslog.INFO +SOUT = idaeslog.DEBUG # Set module level pyest marker pytestmark = pytest.mark.cubic_root @@ -54,7 +54,7 @@ # ----------------------------------------------------------------------------- # Get default solver for testing solver = get_solver() -# Limit iterations to make sure sweeps aren';'t getting out of hand +# Limit iterations to make sure sweeps aren't getting out of hand solver.options["max_iter"] = 50 # --------------------------------------------------------------------- @@ -196,7 +196,7 @@ def m(self): m.fs.props = GenericParameterBlock(**configuration) - m.fs.state = m.fs.props.build_state_block([1], defined_state=False) + m.fs.state = m.fs.props.build_state_block([1], defined_state=True) iscale.calculate_scaling_factors(m.fs.props) iscale.calculate_scaling_factors(m.fs.state[1]) @@ -265,7 +265,13 @@ def test_T350_P1_x5(self, m): m.fs.state.initialize(outlvl=SOUT) - results = solver.solve(m) + from idaes.core.util import DiagnosticsToolbox + + dt = DiagnosticsToolbox(m.fs.state[1]) + dt.report_structural_issues() + dt.display_overconstrained_set() + + results = solver.solve(m, tee=True) # Check for optimal solution assert check_optimal_termination(results) @@ -790,103 +796,3 @@ def test_T376_P1_x2(self, m): assert ( pytest.approx(value(m.fs.state[1].entr_mol_phase["Vap"]), 1e-5) == -273.513 ) - - @pytest.mark.unit - def test_basic_scaling(self, m): - assert len(m.fs.state[1].scaling_factor) == 23 - assert m.fs.state[1].scaling_factor[m.fs.state[1].flow_mol] == 1e-2 - assert m.fs.state[1].scaling_factor[m.fs.state[1].flow_mol_phase["Liq"]] == 1e-2 - assert m.fs.state[1].scaling_factor[m.fs.state[1].flow_mol_phase["Vap"]] == 1e-2 - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].flow_mol_phase_comp["Liq", "benzene"] - ] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].flow_mol_phase_comp["Liq", "toluene"] - ] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].flow_mol_phase_comp["Vap", "benzene"] - ] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].flow_mol_phase_comp["Vap", "toluene"] - ] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[m.fs.state[1].mole_frac_comp["benzene"]] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[m.fs.state[1].mole_frac_comp["toluene"]] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].mole_frac_phase_comp["Liq", "benzene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].mole_frac_phase_comp["Liq", "toluene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].mole_frac_phase_comp["Vap", "benzene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].mole_frac_phase_comp["Vap", "toluene"] - ] - == 1000 - ) - assert m.fs.state[1].scaling_factor[m.fs.state[1].pressure] == 1e-5 - assert m.fs.state[1].scaling_factor[m.fs.state[1].temperature] == 1e-2 - assert m.fs.state[1].scaling_factor[m.fs.state[1]._teq["Vap", "Liq"]] == 1e-2 - assert m.fs.state[1].scaling_factor[m.fs.state[1]._t1_Vap_Liq] == 1e-2 - - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1]._mole_frac_tbub["Vap", "Liq", "benzene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1]._mole_frac_tbub["Vap", "Liq", "toluene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1]._mole_frac_tdew["Vap", "Liq", "benzene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1]._mole_frac_tdew["Vap", "Liq", "toluene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[m.fs.state[1].temperature_bubble["Vap", "Liq"]] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[m.fs.state[1].temperature_dew["Vap", "Liq"]] - == 1e-2 - ) diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 2272a4ae64..82018ee973 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -86,17 +86,11 @@ def phase_equil(b, phase_pair): # Equilibrium temperature def rule_teq(b): - if b.params.get_phase(phase_pair[0]).is_vapor_phase(): - vapor_phase = phase_pair[0] - liquid_phase = phase_pair[1] - else: - vapor_phase = phase_pair[1] - liquid_phase = phase_pair[0] return ( b._teq[phase_pair] - b.temperature - - s[vapor_phase] * t_units - + s[liquid_phase] * t_units + - s[v_phase] * t_units + + s[l_phase] * t_units == 0 ) @@ -170,6 +164,7 @@ def rule_cubic_slack_complementarity(b, p): if b.params.get_phase(p).is_vapor_phase(): return smooth_min(gn[p] * f_units, flow_phase, eps) == 0 else: + # return smooth_min((gp[p] + s[p]) * f_units, flow_phase, eps) == 0 return smooth_min(gp[p] * f_units, flow_phase, eps) == 0 b.add_component( @@ -243,8 +238,13 @@ def phase_equil_initialization(b, phase_pair): for c in b.component_objects(Constraint): # Activate equilibrium constraints - if c.local_name in ("_teq_constraint" + suffix,): - c.deactivate() + if c.local_name in ( + "_teq_constraint" + suffix, + "temperature_slack_complementarity" + suffix, + "cubic_root_complementarity" + suffix, + "cubic_slack_complementarity" + suffix, + ): + c.activate() def _calculate_temperature_slacks(b, phase_pair, liquid_phase, vapor_phase): From 78d437251cc88ea26bed694ba390bd204d94ce72 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 23 Apr 2024 15:56:33 -0400 Subject: [PATCH 10/33] Starting clean up and transition --- .../modular_properties/examples/BT_PR.py | 4 +- .../examples/tests/test_BT_PR.py | 100 ------------------ ...VLE2.py => test_BT_PR_legacy_SmoothVLE.py} | 6 +- .../phase_equil/smooth_VLE_2.py | 28 +++-- .../unit_models/tests/test_heat_exchanger.py | 4 +- .../tests/test_heat_exchanger_1D.py | 4 +- idaes/models/unit_models/tests/test_heater.py | 4 +- .../tests/test_shell_and_tube_1D.py | 4 +- 8 files changed, 32 insertions(+), 122 deletions(-) rename idaes/models/properties/modular_properties/examples/tests/{test_BT_PR_SmoothVLE2.py => test_BT_PR_legacy_SmoothVLE.py} (99%) diff --git a/idaes/models/properties/modular_properties/examples/BT_PR.py b/idaes/models/properties/modular_properties/examples/BT_PR.py index ece424ecb9..1c0710752e 100644 --- a/idaes/models/properties/modular_properties/examples/BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/BT_PR.py @@ -31,7 +31,7 @@ from idaes.models.properties.modular_properties.state_definitions import FTPx from idaes.models.properties.modular_properties.eos.ceos import Cubic, CubicType -from idaes.models.properties.modular_properties.phase_equil import SmoothVLE +from idaes.models.properties.modular_properties.phase_equil import SmoothVLE2 from idaes.models.properties.modular_properties.phase_equil.bubble_dew import ( LogBubbleDew, ) @@ -148,7 +148,7 @@ "temperature_ref": (298.15, pyunits.K), # Defining phase equilibria "phases_in_equilibrium": [("Vap", "Liq")], - "phase_equilibrium_state": {("Vap", "Liq"): SmoothVLE}, + "phase_equilibrium_state": {("Vap", "Liq"): SmoothVLE2}, "bubble_dew_method": LogBubbleDew, "parameter_data": { "PR_kappa": { diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py index 95955efc94..741a040eda 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py @@ -660,103 +660,3 @@ def test_T376_P1_x2(self, m): assert ( pytest.approx(value(m.fs.state[1].entr_mol_phase["Vap"]), 1e-5) == -273.513 ) - - @pytest.mark.unit - def test_basic_scaling(self, m): - assert len(m.fs.state[1].scaling_factor) == 23 - assert m.fs.state[1].scaling_factor[m.fs.state[1].flow_mol] == 1e-2 - assert m.fs.state[1].scaling_factor[m.fs.state[1].flow_mol_phase["Liq"]] == 1e-2 - assert m.fs.state[1].scaling_factor[m.fs.state[1].flow_mol_phase["Vap"]] == 1e-2 - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].flow_mol_phase_comp["Liq", "benzene"] - ] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].flow_mol_phase_comp["Liq", "toluene"] - ] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].flow_mol_phase_comp["Vap", "benzene"] - ] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].flow_mol_phase_comp["Vap", "toluene"] - ] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[m.fs.state[1].mole_frac_comp["benzene"]] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[m.fs.state[1].mole_frac_comp["toluene"]] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].mole_frac_phase_comp["Liq", "benzene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].mole_frac_phase_comp["Liq", "toluene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].mole_frac_phase_comp["Vap", "benzene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1].mole_frac_phase_comp["Vap", "toluene"] - ] - == 1000 - ) - assert m.fs.state[1].scaling_factor[m.fs.state[1].pressure] == 1e-5 - assert m.fs.state[1].scaling_factor[m.fs.state[1].temperature] == 1e-2 - assert m.fs.state[1].scaling_factor[m.fs.state[1]._teq["Vap", "Liq"]] == 1e-2 - assert m.fs.state[1].scaling_factor[m.fs.state[1]._t1_Vap_Liq] == 1e-2 - - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1]._mole_frac_tbub["Vap", "Liq", "benzene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1]._mole_frac_tbub["Vap", "Liq", "toluene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1]._mole_frac_tdew["Vap", "Liq", "benzene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[ - m.fs.state[1]._mole_frac_tdew["Vap", "Liq", "toluene"] - ] - == 1000 - ) - assert ( - m.fs.state[1].scaling_factor[m.fs.state[1].temperature_bubble["Vap", "Liq"]] - == 1e-2 - ) - assert ( - m.fs.state[1].scaling_factor[m.fs.state[1].temperature_dew["Vap", "Liq"]] - == 1e-2 - ) diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_legacy_SmoothVLE.py similarity index 99% rename from idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py rename to idaes/models/properties/modular_properties/examples/tests/test_BT_PR_legacy_SmoothVLE.py index 0b6bd086d5..ac6e48ec1f 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_SmoothVLE2.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_legacy_SmoothVLE.py @@ -36,7 +36,7 @@ from idaes.core import LiquidPhase, VaporPhase, Component from idaes.models.properties.modular_properties.state_definitions import FTPx from idaes.models.properties.modular_properties.eos.ceos import Cubic, CubicType -from idaes.models.properties.modular_properties.phase_equil import SmoothVLE2 +from idaes.models.properties.modular_properties.phase_equil import SmoothVLE from idaes.models.properties.modular_properties.phase_equil.bubble_dew import ( LogBubbleDew, ) @@ -45,7 +45,7 @@ import idaes.logger as idaeslog -SOUT = idaeslog.DEBUG +SOUT = idaeslog.INFO # Set module level pyest marker pytestmark = pytest.mark.cubic_root @@ -162,7 +162,7 @@ "temperature_ref": (298.15, pyunits.K), # Defining phase equilibria "phases_in_equilibrium": [("Vap", "Liq")], - "phase_equilibrium_state": {("Vap", "Liq"): SmoothVLE2}, + "phase_equilibrium_state": {("Vap", "Liq"): SmoothVLE}, "bubble_dew_method": LogBubbleDew, "parameter_data": { "PR_kappa": { diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 82018ee973..13604c5210 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -20,7 +20,15 @@ # TODO: Pylint complains about variables with _x names as they are built by other classes # pylint: disable=protected-access -from pyomo.environ import Constraint, Expression, Param, units as pyunits, Var, value +from pyomo.environ import ( + Constraint, + Expression, + Param, + Set, + units as pyunits, + Var, + value, +) from idaes.core.util.exceptions import ConfigurationError from idaes.core.util.math import smooth_min @@ -74,9 +82,11 @@ def phase_equil(b, phase_pair): t_units = uom.TEMPERATURE f_units = uom.AMOUNT / uom.TIME - # TODO: Do these need to be indexed by ALL phases, or just the VLE phases? + vl_phase_set = Set(initialize=[phase_pair[0], phase_pair[1]]) + b.add_component("_vle_set" + suffix, vl_phase_set) + s = Var( - b.params.phase_list, + vl_phase_set, initialize=0.0, bounds=(0, None), doc="Slack variable for equilibrium temperature", @@ -105,7 +115,7 @@ def rule_teq(b): b.add_component("eps" + suffix, eps) gp = Var( - b.params.phase_list, + vl_phase_set, initialize=0.0, bounds=(0, None), doc="Slack variable for cubic second derivative for phase p", @@ -114,7 +124,7 @@ def rule_teq(b): b.add_component("gp" + suffix, gp) gn = Var( - b.params.phase_list, + vl_phase_set, initialize=0.0, bounds=(0, None), doc="Slack variable for cubic second derivative for phase p", @@ -130,7 +140,7 @@ def rule_temperature_slack_complementarity(b, p): b.add_component( "temperature_slack_complementarity" + suffix, Constraint( - b.params.phase_list, + vl_phase_set, rule=rule_temperature_slack_complementarity, ), ) @@ -147,7 +157,7 @@ def rule_cubic_second_derivative(b, p): b.add_component( "cubic_second_derivative" + suffix, - Expression(b.params.phase_list, rule=rule_cubic_second_derivative), + Expression(vl_phase_set, rule=rule_cubic_second_derivative), ) def rule_cubic_root_complementarity(b, p): @@ -156,7 +166,7 @@ def rule_cubic_root_complementarity(b, p): b.add_component( "cubic_root_complementarity" + suffix, - Constraint(b.params.phase_list, rule=rule_cubic_root_complementarity), + Constraint(vl_phase_set, rule=rule_cubic_root_complementarity), ) def rule_cubic_slack_complementarity(b, p): @@ -169,7 +179,7 @@ def rule_cubic_slack_complementarity(b, p): b.add_component( "cubic_slack_complementarity" + suffix, - Constraint(b.params.phase_list, rule=rule_cubic_slack_complementarity), + Constraint(vl_phase_set, rule=rule_cubic_slack_complementarity), ) @staticmethod diff --git a/idaes/models/unit_models/tests/test_heat_exchanger.py b/idaes/models/unit_models/tests/test_heat_exchanger.py index 1bcf4efd9e..f916fe3fd5 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger.py @@ -1725,8 +1725,8 @@ def test_build(self, btx): assert isinstance(btx.fs.unit.delta_temperature, (Var, Expression)) assert isinstance(btx.fs.unit.heat_transfer_equation, Constraint) - assert number_variables(btx) == 190 - assert number_total_constraints(btx) == 118 + assert number_variables(btx) == 176 + assert number_total_constraints(btx) == 104 assert number_unused_variables(btx) == 20 @pytest.mark.component diff --git a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py index 2f35b2e5f6..7a31e4464f 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py @@ -1912,8 +1912,8 @@ def test_build(self, btx): assert hasattr(btx.fs.unit, "heat_transfer_eq") assert hasattr(btx.fs.unit, "heat_conservation") - assert number_variables(btx) == 1976 - assert number_total_constraints(btx) == 1867 + assert number_variables(btx) == 1829 + assert number_total_constraints(btx) == 1720 assert number_unused_variables(btx) == 36 @pytest.mark.integration diff --git a/idaes/models/unit_models/tests/test_heater.py b/idaes/models/unit_models/tests/test_heater.py index dbee5e970b..74db21657a 100644 --- a/idaes/models/unit_models/tests/test_heater.py +++ b/idaes/models/unit_models/tests/test_heater.py @@ -533,8 +533,8 @@ def test_build(self, btg): assert hasattr(btg.fs.unit, "heat_duty") assert hasattr(btg.fs.unit, "deltaP") - assert number_variables(btg) == 94 - assert number_total_constraints(btg) == 57 + assert number_variables(btg) == 80 + assert number_total_constraints(btg) == 43 # Unused vars are density parameters assert number_unused_variables(btg) == 10 diff --git a/idaes/models/unit_models/tests/test_shell_and_tube_1D.py b/idaes/models/unit_models/tests/test_shell_and_tube_1D.py index 11c14c6dd1..201a6ac9f5 100644 --- a/idaes/models/unit_models/tests/test_shell_and_tube_1D.py +++ b/idaes/models/unit_models/tests/test_shell_and_tube_1D.py @@ -1480,8 +1480,8 @@ def test_build(self, btx): assert hasattr(btx.fs.unit, "cold_side_heat_transfer_eq") assert hasattr(btx.fs.unit, "heat_conservation") - assert number_variables(btx) == 2021 - assert number_total_constraints(btx) == 1890 + assert number_variables(btx) == 1874 + assert number_total_constraints(btx) == 1743 assert number_unused_variables(btx) == 34 @pytest.mark.integration From daf0502382d206e9113b22d5358736d80fff777f Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 29 Apr 2024 11:09:16 -0400 Subject: [PATCH 11/33] Working on Tsweep test failure --- .../properties/modular_properties/eos/ceos.py | 4 +-- .../examples/tests/test_BT_PR.py | 7 ++--- .../phase_equil/smooth_VLE_2.py | 30 +++++++++++-------- .../unit_models/tests/test_heat_exchanger.py | 6 +++- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/idaes/models/properties/modular_properties/eos/ceos.py b/idaes/models/properties/modular_properties/eos/ceos.py index 2be582d984..8115980d12 100644 --- a/idaes/models/properties/modular_properties/eos/ceos.py +++ b/idaes/models/properties/modular_properties/eos/ceos.py @@ -1481,7 +1481,7 @@ def rule_am_default(m, cname, a, p, pp=()): def rule_am_crit_default(m, cname, a_crit): """ - Standard mixing rule for a term at critical point + Standard mixing rule for a term evaluated at critical point """ k = getattr(m.params, cname + "_kappa") return sum( @@ -1505,6 +1505,6 @@ def rule_bm_default(m, b, p): def rule_bm_crit_default(m, b): """ - Standard mixing rule for b term at critical point + Standard mixing rule for b term evaluated at critical point """ return sum(m.mole_frac_comp[i] * b[i] for i in m.component_list) diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py index 741a040eda..80f16d64b4 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py @@ -17,16 +17,15 @@ import pytest +from pyomo.util.check_units import assert_units_consistent +from pyomo.environ import check_optimal_termination, ConcreteModel, Objective, value + from idaes.core import FlowsheetBlock from idaes.models.properties.modular_properties.eos.ceos import cubic_roots_available from idaes.models.properties.modular_properties.examples.BT_PR import configuration from idaes.models.properties.modular_properties.base.generic_property import ( GenericParameterBlock, ) -from pyomo.util.check_units import assert_units_consistent - -from pyomo.environ import check_optimal_termination, ConcreteModel, Objective, value - from idaes.core.solvers import get_solver import idaes.core.util.scaling as iscale from idaes.models.properties.tests.test_harness import PropertyTestHarness diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 13604c5210..7c51296a9f 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -45,6 +45,10 @@ import idaes.core.util.scaling as iscale +# Small value for initializing slack variables +EPS_INIT = 1e-8 + + # ----------------------------------------------------------------------------- class SmoothVLE2: """ @@ -87,7 +91,7 @@ def phase_equil(b, phase_pair): s = Var( vl_phase_set, - initialize=0.0, + initialize=EPS_INIT, bounds=(0, None), doc="Slack variable for equilibrium temperature", units=pyunits.dimensionless, @@ -116,7 +120,7 @@ def rule_teq(b): gp = Var( vl_phase_set, - initialize=0.0, + initialize=EPS_INIT, bounds=(0, None), doc="Slack variable for cubic second derivative for phase p", units=pyunits.dimensionless, @@ -125,7 +129,7 @@ def rule_teq(b): gn = Var( vl_phase_set, - initialize=0.0, + initialize=EPS_INIT, bounds=(0, None), doc="Slack variable for cubic second derivative for phase p", units=pyunits.dimensionless, @@ -264,13 +268,13 @@ def _calculate_temperature_slacks(b, phase_pair, liquid_phase, vapor_phase): if value(b._teq[phase_pair]) > value(b.temperature): s[vapor_phase].set_value(value(b._teq[phase_pair] - b.temperature)) - s[liquid_phase].set_value(0) + s[liquid_phase].set_value(EPS_INIT) elif value(b._teq[phase_pair]) < value(b.temperature): - s[vapor_phase].set_value(0) + s[vapor_phase].set_value(EPS_INIT) s[liquid_phase].set_value(value(b.temperature - b._teq[phase_pair])) else: - s[vapor_phase].set_value(0) - s[liquid_phase].set_value(0) + s[vapor_phase].set_value(EPS_INIT) + s[liquid_phase].set_value(EPS_INIT) def _calculate_ceos_derivative_slacks(b, phase_pair, liquid_phase, vapor_phase): @@ -281,15 +285,15 @@ def _calculate_ceos_derivative_slacks(b, phase_pair, liquid_phase, vapor_phase): der = getattr(b, "cubic_second_derivative" + suffix) if value(der[liquid_phase]) < 0: - gp[liquid_phase].set_value(0) + gp[liquid_phase].set_value(EPS_INIT) gn[liquid_phase].set_value(value(-der[liquid_phase])) else: - gp[liquid_phase].set_value(0) - gn[liquid_phase].set_value(0) + gp[liquid_phase].set_value(EPS_INIT) + gn[liquid_phase].set_value(EPS_INIT) if value(der[vapor_phase]) > 0: gp[vapor_phase].set_value(value(der[vapor_phase])) - gn[vapor_phase].set_value(0) + gn[vapor_phase].set_value(EPS_INIT) else: - gp[vapor_phase].set_value(0) - gn[vapor_phase].set_value(0) + gp[vapor_phase].set_value(EPS_INIT) + gn[vapor_phase].set_value(EPS_INIT) diff --git a/idaes/models/unit_models/tests/test_heat_exchanger.py b/idaes/models/unit_models/tests/test_heat_exchanger.py index f916fe3fd5..fb96853951 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger.py @@ -2082,7 +2082,11 @@ def test_hx0d_initializer(self, model): @pytest.mark.integration def test_block_triangularization(self, model): - initializer = BlockTriangularizationInitializer(constraint_tolerance=2e-5) + import logging + + initializer = BlockTriangularizationInitializer( + constraint_tolerance=2e-5, output_level=logging.DEBUG + ) initializer.initialize(model.fs.unit) assert initializer.summary[model.fs.unit]["status"] == InitializationStatus.Ok From 7ddc0235438949fbfffcde4372c38deef4435238 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 9 May 2024 10:46:26 -0400 Subject: [PATCH 12/33] Trying to debug Tsweep --- .../modular_properties/examples/tests/test_BT_PR.py | 2 +- .../modular_properties/phase_equil/smooth_VLE_2.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py index 80f16d64b4..bf578c3475 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py @@ -41,7 +41,7 @@ # ----------------------------------------------------------------------------- # Get default solver for testing solver = get_solver() -# Limit iterations to make sure sweeps aren;t getting out of hand +# Limit iterations to make sure sweeps aren't getting out of hand solver.options["max_iter"] = 50 diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 7c51296a9f..7a8831e338 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -17,8 +17,6 @@ A complementarity-based vapor-liquid equilibrium formulation for equation-oriented simulation and optimization. AIChE Journal, DOI: 10.1002/aic.18029 """ -# TODO: Pylint complains about variables with _x names as they are built by other classes -# pylint: disable=protected-access from pyomo.environ import ( Constraint, @@ -46,7 +44,7 @@ # Small value for initializing slack variables -EPS_INIT = 1e-8 +EPS_INIT = 1e-4 # ----------------------------------------------------------------------------- @@ -178,7 +176,6 @@ def rule_cubic_slack_complementarity(b, p): if b.params.get_phase(p).is_vapor_phase(): return smooth_min(gn[p] * f_units, flow_phase, eps) == 0 else: - # return smooth_min((gp[p] + s[p]) * f_units, flow_phase, eps) == 0 return smooth_min(gp[p] * f_units, flow_phase, eps) == 0 b.add_component( From 81d9d26ef6ec90e6dd761f18c2b17440f7702415 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 9 May 2024 11:09:15 -0400 Subject: [PATCH 13/33] Catch for non-cubics with SmoothVLE2 --- .../phase_equil/smooth_VLE_2.py | 26 +++- .../phase_equil/tests/test_smooth_VLE_2.py | 132 ++++++++++++++++-- 2 files changed, 141 insertions(+), 17 deletions(-) diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 7a8831e338..854115e829 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -71,13 +71,29 @@ def phase_equil(b, phase_pair): if l_phase is None or v_phase is None: raise ConfigurationError( - f"{b.params.name} Generic Property Package phase pair {phase_pair[0]}-{phase_pair[1]} " + f"{b.params.name} - Generic Property Package phase pair {phase_pair[0]}-{phase_pair[1]} " "was set to use Smooth VLE formulation, however this is not a vapor-liquid pair." ) - lobj = b.params.get_phase(l_phase) - ctype = lobj._cubic_type - cname = lobj.config.equation_of_state_options["type"].name + try: + lobj = b.params.get_phase(l_phase) + ctype = lobj._cubic_type + cname = lobj.config.equation_of_state_options["type"].name + vobj = b.params.get_phase(v_phase) + + if ( + ctype != vobj._cubic_type + or lobj.config.equation_of_state_options + != vobj.config.equation_of_state_options + ): + raise ConfigurationError( + f"{b.params.name} - SmoothVLE2 formulation requires that both phases use the same " + "type of cubic equation of state." + ) + except AttributeError: + raise ConfigurationError( + f"{b.params.name} - SmoothVLE2 formulation only supports cubic equations of state." + ) # Definition of equilibrium temperature for smooth VLE uom = b.params.get_metadata().default_units @@ -213,7 +229,7 @@ def calculate_teq(blk, pp): if v_only_comps is None: if blk.is_property_constructed("temperature_bubble"): - Tbub = value(blk.temeprature_bubble[pp]) + Tbub = value(blk.temperature_bubble[pp]) else: Tbub = estimate_Tbub( blk, T_units, raoult_comps, henry_comps, liquid_phase diff --git a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py index 23243e157e..c9de89e871 100644 --- a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py @@ -36,9 +36,117 @@ SmoothVLE2, _calculate_temperature_slacks, _calculate_ceos_derivative_slacks, + EPS_INIT, ) +from idaes.models.properties.modular_properties.eos.ideal import Ideal from idaes.models.properties.modular_properties.eos.ceos import Cubic, CubicType from idaes.models.properties.modular_properties.phase_equil.forms import fugacity +from idaes.core.util.exceptions import ConfigurationError + + +@pytest.mark.unit +def test_different_cubics(): + m = ConcreteModel() + + m.params = GenericParameterBlock( + components={ + "H2O": { + "parameter_data": { + "pressure_crit": (220.6e5, pyunits.Pa), + "temperature_crit": (647, pyunits.K), + "omega": 0.344, + }, + "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, + } + }, + phases={ + "Liq": { + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + "Vap": { + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.SRK}, + }, + }, + state_definition=FTPx, + pressure_ref=100000.0, + temperature_ref=300, + base_units={ + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + }, + phases_in_equilibrium=[("Vap", "Liq")], + phase_equilibrium_state={("Vap", "Liq"): SmoothVLE2}, + parameter_data={ + "PR_kappa": { + ("H2O", "H2O"): 0.000, + }, + "SRK_kappa": { + ("H2O", "H2O"): 0.000, + }, + }, + ) + + with pytest.raises( + ConfigurationError, + match="params - SmoothVLE2 formulation requires that both phases use the same " + "type of cubic equation of state.", + ): + m.props = m.params.state_block_class([1], parameters=m.params) + + +@pytest.mark.unit +def test_non_cubic(): + m = ConcreteModel() + + m.params = GenericParameterBlock( + components={ + "H2O": { + "parameter_data": { + "pressure_crit": (220.6e5, pyunits.Pa), + "temperature_crit": (647, pyunits.K), + "omega": 0.344, + }, + "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, + } + }, + phases={ + "Liq": { + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + "Vap": { + "equation_of_state": Ideal, + }, + }, + state_definition=FTPx, + pressure_ref=100000.0, + temperature_ref=300, + base_units={ + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + }, + phases_in_equilibrium=[("Vap", "Liq")], + phase_equilibrium_state={("Vap", "Liq"): SmoothVLE2}, + parameter_data={ + "PR_kappa": { + ("H2O", "H2O"): 0.000, + } + }, + ) + + with pytest.raises( + ConfigurationError, + match="params - SmoothVLE2 formulation only supports cubic equations of state.", + ): + m.props = m.params.state_block_class([1], parameters=m.params) @pytest.fixture() @@ -153,21 +261,21 @@ def test_calculate_temperature_slacks(frame): _calculate_temperature_slacks(frame.props[1], ("Vap", "Liq"), "Liq", "Vap") assert value(s["Vap"]) == pytest.approx(100, rel=1e-8) - assert value(s["Liq"]) == pytest.approx(0, abs=1e-8) + assert value(s["Liq"]) == pytest.approx(EPS_INIT, abs=1e-8) # Teq < T frame.props[1]._teq[("Vap", "Liq")].set_value(200) _calculate_temperature_slacks(frame.props[1], ("Vap", "Liq"), "Liq", "Vap") assert value(s["Liq"]) == pytest.approx(100, rel=1e-8) - assert value(s["Vap"]) == pytest.approx(0, abs=1e-8) + assert value(s["Vap"]) == pytest.approx(EPS_INIT, abs=1e-8) # Teq == T frame.props[1]._teq[("Vap", "Liq")].set_value(300) _calculate_temperature_slacks(frame.props[1], ("Vap", "Liq"), "Liq", "Vap") - assert value(s["Liq"]) == pytest.approx(0, abs=1e-8) - assert value(s["Vap"]) == pytest.approx(0, abs=1e-8) + assert value(s["Liq"]) == pytest.approx(EPS_INIT, abs=1e-8) + assert value(s["Vap"]) == pytest.approx(EPS_INIT, abs=1e-8) @pytest.mark.unit @@ -192,11 +300,11 @@ def test_calculate_ceos_derivative_slacks(frame): der_l = value(6 * Z["Liq"] + 2 * -(1 + B - 2 * B)) der_v = value(6 * Z["Vap"] + 2 * -(1 + B - 2 * B)) - assert value(gp["Liq"]) == pytest.approx(0, abs=1e-8) + assert value(gp["Liq"]) == pytest.approx(EPS_INIT, abs=1e-8) assert value(gn["Liq"]) == pytest.approx(-der_l, rel=1e-8) assert value(gp["Vap"]) == pytest.approx(der_v, rel=1e-8) - assert value(gn["Vap"]) == pytest.approx(0, abs=1e-8) + assert value(gn["Vap"]) == pytest.approx(EPS_INIT, abs=1e-8) # Supercritical conditions, vapor derivative +ve, liquid +ve frame.props[1].pressure.set_value(250e5) @@ -208,11 +316,11 @@ def test_calculate_ceos_derivative_slacks(frame): B = value(b * frame.props[1].pressure / R / frame.props[1].temperature) der_v = value(6 * Z["Vap"] + 2 * -(1 + B - 2 * B)) - assert value(gp["Liq"]) == pytest.approx(0, abs=1e-8) - assert value(gn["Liq"]) == pytest.approx(0, abs=1e-8) + assert value(gp["Liq"]) == pytest.approx(EPS_INIT, abs=1e-8) + assert value(gn["Liq"]) == pytest.approx(EPS_INIT, abs=1e-8) assert value(gp["Vap"]) == pytest.approx(der_v, rel=1e-8) - assert value(gn["Vap"]) == pytest.approx(0, abs=1e-8) + assert value(gn["Vap"]) == pytest.approx(EPS_INIT, abs=1e-8) # Supercritical conditions, vapor derivative -ve, liquid -ve frame.props[1].pressure.set_value(250e5) @@ -224,8 +332,8 @@ def test_calculate_ceos_derivative_slacks(frame): B = value(b * frame.props[1].pressure / R / frame.props[1].temperature) der_l = value(6 * Z["Liq"] + 2 * -(1 + B - 2 * B)) - assert value(gp["Liq"]) == pytest.approx(0, abs=1e-8) + assert value(gp["Liq"]) == pytest.approx(EPS_INIT, abs=1e-8) assert value(gn["Liq"]) == pytest.approx(-der_l, rel=1e-8) - assert value(gp["Vap"]) == pytest.approx(0, abs=1e-8) - assert value(gn["Vap"]) == pytest.approx(0, abs=1e-8) + assert value(gp["Vap"]) == pytest.approx(EPS_INIT, abs=1e-8) + assert value(gn["Vap"]) == pytest.approx(EPS_INIT, abs=1e-8) From f4eb3799bc2feb586f2b74f1681a6bada458500e Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 9 May 2024 11:11:20 -0400 Subject: [PATCH 14/33] Fixing typo --- .../properties/modular_properties/phase_equil/smooth_VLE_2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 854115e829..91674c65ed 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -240,7 +240,7 @@ def calculate_teq(blk, pp): if v_only_comps is None: if blk.is_property_constructed("temperature_dew"): - Tdew = value(blk.temeprature_bubble[pp]) + Tdew = value(blk.temperature_bubble[pp]) else: Tdew = estimate_Tdew( blk, T_units, raoult_comps, henry_comps, liquid_phase From 860ad56d2c298010311b6f69fe90e99c4ca92798 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Fri, 17 May 2024 15:43:16 -0400 Subject: [PATCH 15/33] Tweaking eps values --- .../examples/tests/test_BT_PR.py | 10 +++++++++- .../phase_equil/smooth_VLE_2.py | 20 +++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py index bf578c3475..9ccda56e4f 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py @@ -93,7 +93,15 @@ def test_T_sweep(self, m): m.fs.state[1].temperature.unfix() m.fs.obj.activate() - results = solver.solve(m) + results = solver.solve(m, tee=True) + + m.fs.state.display() + from idaes.core.util import DiagnosticsToolbox + + dt = DiagnosticsToolbox(m.fs) + dt.report_structural_issues() + dt.report_numerical_issues() + dt.display_variables_at_or_outside_bounds() assert check_optimal_termination(results) assert m.fs.state[1].flow_mol_phase["Liq"].value <= 1e-5 diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 91674c65ed..154879b876 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -124,13 +124,21 @@ def rule_teq(b): b.add_component("_teq_constraint" + suffix, Constraint(rule=rule_teq)) - eps = Param( - default=1e-04, + eps1 = Param( + default=1e-4, mutable=True, doc="Smoothing parameter for complementarities", units=f_units, ) - b.add_component("eps" + suffix, eps) + b.add_component("eps_1" + suffix, eps1) + + eps2 = Param( + default=1e-5, + mutable=True, + doc="Smoothing parameter for complementarities", + units=f_units, + ) + b.add_component("eps_2" + suffix, eps2) gp = Var( vl_phase_set, @@ -153,7 +161,7 @@ def rule_teq(b): def rule_temperature_slack_complementarity(b, p): flow_phase = b.flow_mol_phase[p] - return smooth_min(s[p] * f_units, flow_phase, eps) == 0 + return smooth_min(s[p] * f_units, flow_phase, eps1) == 0 b.add_component( "temperature_slack_complementarity" + suffix, @@ -190,9 +198,9 @@ def rule_cubic_root_complementarity(b, p): def rule_cubic_slack_complementarity(b, p): flow_phase = b.flow_mol_phase[p] if b.params.get_phase(p).is_vapor_phase(): - return smooth_min(gn[p] * f_units, flow_phase, eps) == 0 + return smooth_min(gn[p] * f_units, flow_phase, eps2) == 0 else: - return smooth_min(gp[p] * f_units, flow_phase, eps) == 0 + return smooth_min(gp[p] * f_units, flow_phase, eps2) == 0 b.add_component( "cubic_slack_complementarity" + suffix, From 92394ee5c52e5fbbebaa95a1687e415ee283aa7f Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 20 May 2024 12:33:23 -0400 Subject: [PATCH 16/33] Fixing T sweep test --- .../examples/tests/test_BT_PR.py | 38 ++++++++++--------- .../phase_equil/smooth_VLE_2.py | 2 +- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py index 9ccda56e4f..b39b7c0213 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py @@ -18,7 +18,7 @@ import pytest from pyomo.util.check_units import assert_units_consistent -from pyomo.environ import check_optimal_termination, ConcreteModel, Objective, value +from pyomo.environ import assert_optimal_termination, ConcreteModel, Objective, value from idaes.core import FlowsheetBlock from idaes.models.properties.modular_properties.eos.ceos import cubic_roots_available @@ -88,22 +88,26 @@ def test_T_sweep(self, m): m.fs.state[1].temperature.fix(300) m.fs.state[1].pressure.fix(10 ** (0.5 * logP)) + # For optimization sweep, use a large eps to avoid getting stuck at + # bubble and dew points + m.fs.state[1].eps_1_Vap_Liq.set_value(10) + m.fs.state[1].eps_2_Vap_Liq.set_value(10) + m.fs.state.initialize() m.fs.state[1].temperature.unfix() m.fs.obj.activate() - results = solver.solve(m, tee=True) + results = solver.solve(m) + assert_optimal_termination(results) - m.fs.state.display() - from idaes.core.util import DiagnosticsToolbox + # Switch to small eps and re-solve to refine result + m.fs.state[1].eps_1_Vap_Liq.set_value(1e-4) + m.fs.state[1].eps_2_Vap_Liq.set_value(1e-4) - dt = DiagnosticsToolbox(m.fs) - dt.report_structural_issues() - dt.report_numerical_issues() - dt.display_variables_at_or_outside_bounds() + results = solver.solve(m) - assert check_optimal_termination(results) + assert_optimal_termination(results) assert m.fs.state[1].flow_mol_phase["Liq"].value <= 1e-5 @pytest.mark.integration @@ -119,12 +123,12 @@ def test_P_sweep(self, m): results = solver.solve(m) - assert check_optimal_termination(results) + assert_optimal_termination(results) while m.fs.state[1].pressure.value <= 1e6: results = solver.solve(m) - assert check_optimal_termination(results) + assert_optimal_termination(results) m.fs.state[1].pressure.value = m.fs.state[1].pressure.value + 1e5 @@ -145,7 +149,7 @@ def test_T350_P1_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert check_optimal_termination(results) + assert_optimal_termination(results) assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), abs=1e-1) == 365 assert 0.0035346 == pytest.approx( @@ -234,7 +238,7 @@ def test_T350_P5_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert check_optimal_termination(results) + assert_optimal_termination(results) assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 431.47 assert ( @@ -325,7 +329,7 @@ def test_T450_P1_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert check_optimal_termination(results) + assert_optimal_termination(results) assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 371.4 assert 0.0033583 == pytest.approx( @@ -414,7 +418,7 @@ def test_T450_P5_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert check_optimal_termination(results) + assert_optimal_termination(results) assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 436.93 assert 0.0166181 == pytest.approx( @@ -503,7 +507,7 @@ def test_T368_P1_x5(self, m): results = solver.solve(m) # Check for optimal solution - assert check_optimal_termination(results) + assert_optimal_termination(results) assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 368 assert 0.003504 == pytest.approx( @@ -596,7 +600,7 @@ def test_T376_P1_x2(self, m): results = solver.solve(m) # Check for optimal solution - assert check_optimal_termination(results) + assert_optimal_termination(results) assert pytest.approx(value(m.fs.state[1]._teq[("Vap", "Liq")]), 1e-5) == 376 assert 0.00361333 == pytest.approx( diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 154879b876..5f828767e8 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -133,7 +133,7 @@ def rule_teq(b): b.add_component("eps_1" + suffix, eps1) eps2 = Param( - default=1e-5, + default=1e-4, mutable=True, doc="Smoothing parameter for complementarities", units=f_units, From 65087b65a32d9236665fe191ba83916f9aaa4779 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 21 May 2024 13:55:00 -0400 Subject: [PATCH 17/33] Fixing some tests --- idaes/core/util/model_diagnostics.py | 54 +++++++++++-------- .../phase_equil/tests/test_smooth_VLE_2.py | 6 ++- .../unit_models/tests/test_heat_exchanger.py | 8 +-- .../tests/test_heat_exchanger_1D.py | 4 +- idaes/models/unit_models/tests/test_heater.py | 4 +- .../tests/test_shell_and_tube_1D.py | 4 +- 6 files changed, 49 insertions(+), 31 deletions(-) diff --git a/idaes/core/util/model_diagnostics.py b/idaes/core/util/model_diagnostics.py index 92ca93da3b..49ccebe679 100644 --- a/idaes/core/util/model_diagnostics.py +++ b/idaes/core/util/model_diagnostics.py @@ -1148,7 +1148,9 @@ def _collect_structural_cautions(self): return cautions - def _collect_numerical_warnings(self, jac=None, nlp=None): + def _collect_numerical_warnings( + self, jac=None, nlp=None, ignore_parallel_components=False + ): """ Runs checks for numerical warnings and returns two lists. @@ -1236,27 +1238,30 @@ def _collect_numerical_warnings(self, jac=None, nlp=None): ) # Parallel variables and constraints - partol = self.config.parallel_component_tolerance - par_cons = check_parallel_jacobian( - self._model, tolerance=partol, direction="row", jac=jac, nlp=nlp - ) - par_vars = check_parallel_jacobian( - self._model, tolerance=partol, direction="column", jac=jac, nlp=nlp - ) - if par_cons: - p = "pair" if len(par_cons) == 1 else "pairs" - warnings.append( - f"WARNING: {len(par_cons)} {p} of constraints are parallel" - f" (to tolerance {partol:.1E})" + if not ignore_parallel_components: + partol = self.config.parallel_component_tolerance + par_cons = check_parallel_jacobian( + self._model, tolerance=partol, direction="row", jac=jac, nlp=nlp ) - next_steps.append(self.display_near_parallel_constraints.__name__ + "()") - if par_vars: - p = "pair" if len(par_vars) == 1 else "pairs" - warnings.append( - f"WARNING: {len(par_vars)} {p} of variables are parallel" - f" (to tolerance {partol:.1E})" + par_vars = check_parallel_jacobian( + self._model, tolerance=partol, direction="column", jac=jac, nlp=nlp ) - next_steps.append(self.display_near_parallel_variables.__name__ + "()") + if par_cons: + p = "pair" if len(par_cons) == 1 else "pairs" + warnings.append( + f"WARNING: {len(par_cons)} {p} of constraints are parallel" + f" (to tolerance {partol:.1E})" + ) + next_steps.append( + self.display_near_parallel_constraints.__name__ + "()" + ) + if par_vars: + p = "pair" if len(par_vars) == 1 else "pairs" + warnings.append( + f"WARNING: {len(par_vars)} {p} of variables are parallel" + f" (to tolerance {partol:.1E})" + ) + next_steps.append(self.display_near_parallel_variables.__name__ + "()") return warnings, next_steps @@ -1404,16 +1409,21 @@ def assert_no_structural_warnings( if len(warnings) > 0: raise AssertionError(f"Structural issues found ({len(warnings)}).") - def assert_no_numerical_warnings(self): + def assert_no_numerical_warnings(self, ignore_parallel_components=False): """ Checks for numerical warnings in the model and raises an AssertionError if any are found. + Args: + ignore_parallel_components - ignore checks for parallel components + Raises: AssertionError if any warnings are identified by numerical analysis. """ - warnings, _ = self._collect_numerical_warnings() + warnings, _ = self._collect_numerical_warnings( + ignore_parallel_components=ignore_parallel_components + ) if len(warnings) > 0: raise AssertionError(f"Numerical issues found ({len(warnings)}).") diff --git a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py index c9de89e871..e349fd81a6 100644 --- a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py @@ -208,8 +208,10 @@ def test_build(frame): assert isinstance(frame.props[1].cubic_second_derivative_Vap_Liq, Expression) - assert isinstance(frame.props[1].eps_Vap_Liq, Param) - assert value(frame.props[1].eps_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) + assert isinstance(frame.props[1].eps_1_Vap_Liq, Param) + assert value(frame.props[1].eps_1_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) + assert isinstance(frame.props[1].eps_2_Vap_Liq, Param) + assert value(frame.props[1].eps_2_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) assert isinstance(frame.props[1]._teq_constraint_Vap_Liq, Constraint) assert isinstance( diff --git a/idaes/models/unit_models/tests/test_heat_exchanger.py b/idaes/models/unit_models/tests/test_heat_exchanger.py index fb96853951..16ddfaed1e 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger.py @@ -1893,7 +1893,9 @@ def test_conservation(self, btx): @pytest.mark.component def test_numerical_issues(self, btx): dt = DiagnosticsToolbox(btx) - dt.assert_no_numerical_warnings() + # TODO: Complementarity formulation results in near-parallel components + # when unscaled + dt.assert_no_numerical_warnings(ignore_parallel_components=True) @pytest.mark.component def test_initialization_error(self, btx): @@ -2082,10 +2084,8 @@ def test_hx0d_initializer(self, model): @pytest.mark.integration def test_block_triangularization(self, model): - import logging - initializer = BlockTriangularizationInitializer( - constraint_tolerance=2e-5, output_level=logging.DEBUG + constraint_tolerance=2e-5, ) initializer.initialize(model.fs.unit) diff --git a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py index 58d3e163c7..7a4cd24ef4 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py @@ -2641,7 +2641,9 @@ def test_conservation(self, btx): @pytest.mark.integration def test_numerical_issues(self, btx): dt = DiagnosticsToolbox(btx) - dt.assert_no_numerical_warnings() + # TODO: Complementarity formulation results in near-parallel components + # when unscaled + dt.assert_no_numerical_warnings(ignore_parallel_components=True) @pytest.mark.component def test_initialization_error(self, btx): diff --git a/idaes/models/unit_models/tests/test_heater.py b/idaes/models/unit_models/tests/test_heater.py index 74db21657a..4657beb558 100644 --- a/idaes/models/unit_models/tests/test_heater.py +++ b/idaes/models/unit_models/tests/test_heater.py @@ -595,7 +595,9 @@ def test_conservation(self, btg): @pytest.mark.component def test_numerical_issues(self, btg): dt = DiagnosticsToolbox(btg) - dt.assert_no_numerical_warnings() + # TODO: Complementarity formulation results in near-parallel components + # when unscaled + dt.assert_no_numerical_warnings(ignore_parallel_components=True) @pytest.mark.ui @pytest.mark.unit diff --git a/idaes/models/unit_models/tests/test_shell_and_tube_1D.py b/idaes/models/unit_models/tests/test_shell_and_tube_1D.py index 6889473329..0c7207f576 100644 --- a/idaes/models/unit_models/tests/test_shell_and_tube_1D.py +++ b/idaes/models/unit_models/tests/test_shell_and_tube_1D.py @@ -1625,7 +1625,9 @@ def test_conservation(self, btx): @pytest.mark.integration def test_numerical_issues(self, btx): dt = DiagnosticsToolbox(btx) - dt.assert_no_numerical_warnings() + # TODO: Complementarity formulation results in near-parallel components + # when unscaled + dt.assert_no_numerical_warnings(ignore_parallel_components=True) @pytest.mark.component def test_initialization_error(self, btx): From f2b98136b7bec4ab2d5145e7f5dd120c8741a26e Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 21 May 2024 14:34:19 -0400 Subject: [PATCH 18/33] Fixing some pylint issues --- .../modular_properties/base/utility.py | 4 +- .../properties/modular_properties/eos/ceos.py | 39 +++++++++++++ .../phase_equil/bubble_dew.py | 56 +++++++++++++------ .../phase_equil/smooth_VLE.py | 16 +++++- .../phase_equil/smooth_VLE_2.py | 48 +++++++++++----- 5 files changed, 131 insertions(+), 32 deletions(-) diff --git a/idaes/models/properties/modular_properties/base/utility.py b/idaes/models/properties/modular_properties/base/utility.py index 953392f055..3006ea823c 100644 --- a/idaes/models/properties/modular_properties/base/utility.py +++ b/idaes/models/properties/modular_properties/base/utility.py @@ -265,7 +265,9 @@ def get_concentration_term(blk, r_idx, log=False): conc_form = cfg.inherent_reactions[r_idx].concentration_form state = blk - if hasattr(state.params, "_electrolyte") and state.params._electrolyte: + if ( + hasattr(state.params, "_electrolyte") and state.params._electrolyte + ): # pylint: disable=protected-access sub = "_true" else: sub = "" diff --git a/idaes/models/properties/modular_properties/eos/ceos.py b/idaes/models/properties/modular_properties/eos/ceos.py index 8115980d12..587568dccc 100644 --- a/idaes/models/properties/modular_properties/eos/ceos.py +++ b/idaes/models/properties/modular_properties/eos/ceos.py @@ -540,6 +540,9 @@ def build_parameters(b): @staticmethod def compress_fact_phase(b, p): + """ + Compressibility factor + """ pobj = b.params.get_phase(p) cname = pobj._cubic_type.name A = getattr(b, cname + "_A") @@ -559,10 +562,16 @@ def compress_fact_phase(b, p): @staticmethod def cp_mass_phase(blk, p): + """ + Phase mass-specific heat capacity at constant pressure + """ return blk.cp_mol_phase[p] / blk.mw_phase[p] @staticmethod def cp_mol_phase(blk, p): + """ + Phase molar heat capacity at constant pressure + """ pobj = blk.params.get_phase(p) cname = pobj._cubic_type.name @@ -604,10 +613,16 @@ def cp_mol_phase(blk, p): @staticmethod def cv_mass_phase(blk, p): + """ + Phase mass-specific heat capacity at constant volume + """ return blk.cv_mol_phase[p] / blk.mw_phase[p] @staticmethod def cv_mol_phase(blk, p): + """ + Phase molar heat capacity at constant volume + """ pobj = blk.params.get_phase(p) cname = pobj._cubic_type.name am = getattr(blk, cname + "_am")[p] @@ -632,16 +647,25 @@ def cv_mol_phase(blk, p): @staticmethod def dens_mass_phase(b, p): + """ + Phase density (mass basis) + """ return b.dens_mol_phase[p] * b.mw_phase[p] @staticmethod def dens_mol_phase(b, p): + """ + Phase density (mole basis) + """ return b.pressure / ( Cubic.gas_constant(b) * b.temperature * b.compress_fact_phase[p] ) @staticmethod def energy_internal_mol_phase(blk, p): + """ + Phase specific internal energy + """ pobj = blk.params.get_phase(p) cname = pobj._cubic_type.name @@ -671,12 +695,18 @@ def energy_internal_mol_phase(blk, p): @staticmethod def energy_internal_mol_phase_comp(blk, p, j): + """ + Phase partial specific internal energy + """ return ( blk.enth_mol_phase_comp[p, j] - blk.pressure * blk.vol_mol_phase_comp[p, j] ) @staticmethod def enth_mol_phase(blk, p): + """ + Phase specific enthalpy + """ pobj = blk.params.get_phase(p) cname = pobj._cubic_type.name @@ -706,6 +736,9 @@ def enth_mol_phase(blk, p): @staticmethod def enth_mol_phase_comp(blk, p, j): + """ + Phase partial molar enthalpy + """ dlogphi_j_dT = _d_log_fug_coeff_dT_phase_comp(blk, p, j) enth_ideal_gas = get_method(blk, "enth_mol_ig_comp", j)( @@ -718,6 +751,9 @@ def enth_mol_phase_comp(blk, p, j): @staticmethod def entr_mol_phase(blk, p): + """ + Phase specific entropy + """ pobj = blk.params.get_phase(p) cname = pobj._cubic_type.name @@ -756,6 +792,9 @@ def entr_mol_phase(blk, p): @staticmethod def entr_mol_phase_comp(blk, p, j): + """ + Phase partial molar entropy + """ logphi_j = _log_fug_coeff_phase_comp(blk, p, j) dlogphi_j_dT = _d_log_fug_coeff_dT_phase_comp(blk, p, j) diff --git a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py index 16d0e10ef5..6cfa19fc4a 100644 --- a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py +++ b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py @@ -38,6 +38,9 @@ class IdealBubbleDew: # calculate concentrations at the bubble and dew points @staticmethod def temperature_bubble(b): + """ + Rule for calculating bubble temeprature + """ _non_vle_phase_check(b) try: @@ -47,7 +50,7 @@ def rule_bubble_temp(b, p1, p2): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, (p1, p2)) @@ -90,7 +93,7 @@ def rule_mole_frac_bubble_temp(b, p1, p2, j): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, (p1, p2)) @@ -124,6 +127,9 @@ def rule_mole_frac_bubble_temp(b, p1, p2, j): @staticmethod def scale_temperature_bubble(b, overwrite=True): + """ + Scaling method for bubble temperature + """ sf_P = iscale.get_scaling_factor(b.pressure, default=1e-5, warning=True) sf_mf = iscale.get_scaling_factor(b.mole_frac_comp, default=1e3, warning=True) @@ -133,8 +139,8 @@ def scale_temperature_bubble(b, overwrite=True): l_phase, v_phase, vl_comps, - henry_comps, - l_only_comps, + _, + _, v_only_comps, ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: @@ -158,6 +164,9 @@ def scale_temperature_bubble(b, overwrite=True): # Dew temperature methods @staticmethod def temperature_dew(b): + """ + Method for calculating dew temperature + """ _non_vle_phase_check(b) try: @@ -168,7 +177,7 @@ def rule_dew_temp(b, p1, p2): vl_comps, henry_comps, l_only_comps, - v_only_comps, + _, ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: @@ -213,7 +222,7 @@ def rule_mole_frac_dew_temp(b, p1, p2, j): vl_comps, henry_comps, l_only_comps, - v_only_comps, + _, ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: @@ -248,6 +257,9 @@ def rule_mole_frac_dew_temp(b, p1, p2, j): @staticmethod def scale_temperature_dew(b, overwrite=True): + """ + Scaling method for dew temperature + """ sf_P = iscale.get_scaling_factor(b.pressure, default=1e-5, warning=True) sf_mf = iscale.get_scaling_factor(b.mole_frac_comp, default=1e3, warning=True) @@ -257,8 +269,8 @@ def scale_temperature_dew(b, overwrite=True): l_phase, v_phase, vl_comps, - henry_comps, - l_only_comps, + _, + _, v_only_comps, ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: @@ -280,6 +292,9 @@ def scale_temperature_dew(b, overwrite=True): # Bubble pressure methods @staticmethod def pressure_bubble(b): + """ + Method for calculating bubble pressure + """ _non_vle_phase_check(b) try: @@ -289,7 +304,7 @@ def rule_bubble_press(b, p1, p2): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, (p1, p2)) @@ -321,7 +336,7 @@ def rule_mole_frac_bubble_press(b, p1, p2, j): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, (p1, p2)) @@ -351,6 +366,9 @@ def rule_mole_frac_bubble_press(b, p1, p2, j): @staticmethod def scale_pressure_bubble(b, overwrite=True): + """ + Scaling method for bubble pressure + """ sf_P = iscale.get_scaling_factor(b.pressure, default=1e-5, warning=True) sf_mf = iscale.get_scaling_factor(b.mole_frac_comp, default=1e3, warning=True) @@ -360,8 +378,8 @@ def scale_pressure_bubble(b, overwrite=True): l_phase, v_phase, vl_comps, - henry_comps, - l_only_comps, + _, + _, v_only_comps, ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: @@ -385,6 +403,9 @@ def scale_pressure_bubble(b, overwrite=True): # Dew pressure methods @staticmethod def pressure_dew(b): + """ + Method for calculating dew pressure + """ _non_vle_phase_check(b) try: @@ -395,7 +416,7 @@ def rule_dew_press(b, p1, p2): vl_comps, henry_comps, l_only_comps, - v_only_comps, + _, ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: @@ -425,7 +446,7 @@ def rule_mole_frac_dew_press(b, p1, p2, j): vl_comps, henry_comps, l_only_comps, - v_only_comps, + _, ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: @@ -454,6 +475,9 @@ def rule_mole_frac_dew_press(b, p1, p2, j): @staticmethod def scale_pressure_dew(b, overwrite=True): + """ + Scaling method for dew pressure + """ sf_P = iscale.get_scaling_factor(b.pressure, default=1e-5, warning=True) sf_mf = iscale.get_scaling_factor(b.mole_frac_comp, default=1e3, warning=True) @@ -463,8 +487,8 @@ def scale_pressure_dew(b, overwrite=True): l_phase, v_phase, vl_comps, - henry_comps, - l_only_comps, + _, + _, v_only_comps, ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py index d51d411c30..39eedbe205 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE.py @@ -35,14 +35,17 @@ class SmoothVLE(object): @staticmethod def phase_equil(b, phase_pair): + """ + Method for constructing phase equilibrium variables and constraints + """ # This method is called via StateBlock.build, thus does not need clean-up # try/except statements suffix = "_" + phase_pair[0] + "_" + phase_pair[1] # Smooth VLE assumes a liquid and a vapor phase, so validate this ( - l_phase, - v_phase, + _, + _, _, _, l_only_comps, @@ -116,6 +119,9 @@ def rule_teq(b): @staticmethod def calculate_scaling_factors(b, phase_pair): + """ + Method to calculate scaling factors for phase equilibrium + """ suffix = "_" + phase_pair[0] + "_" + phase_pair[1] sf_T = iscale.get_scaling_factor(b.temperature, default=1, warning=True) @@ -132,6 +138,9 @@ def calculate_scaling_factors(b, phase_pair): @staticmethod def phase_equil_initialization(b, phase_pair): + """ + Method to initialize phase equilibrium + """ suffix = "_" + phase_pair[0] + "_" + phase_pair[1] for c in b.component_objects(Constraint): @@ -141,6 +150,9 @@ def phase_equil_initialization(b, phase_pair): @staticmethod def calculate_teq(b, phase_pair): + """ + Method to calculate initial guess for equilibrium temperature + """ suffix = "_" + phase_pair[0] + "_" + phase_pair[1] if hasattr(b, "eq_temperature_bubble"): diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 5f828767e8..ac52083781 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -55,6 +55,9 @@ class SmoothVLE2: @staticmethod def phase_equil(b, phase_pair): + """ + Method for constructing phase equilibrium variables and constraints + """ # This method is called via StateBlock.build, thus does not need clean-up # try/except statements suffix = "_" + phase_pair[0] + "_" + phase_pair[1] @@ -63,10 +66,10 @@ def phase_equil(b, phase_pair): ( l_phase, v_phase, - vl_comps, - henry_comps, - l_only_comps, - v_only_comps, + _, + _, + _, + _, ) = identify_VL_component_list(b, phase_pair) if l_phase is None or v_phase is None: @@ -77,12 +80,12 @@ def phase_equil(b, phase_pair): try: lobj = b.params.get_phase(l_phase) - ctype = lobj._cubic_type + ctype = lobj._cubic_type # pylint: disable=protected-access cname = lobj.config.equation_of_state_options["type"].name vobj = b.params.get_phase(v_phase) if ( - ctype != vobj._cubic_type + ctype != vobj._cubic_type # pylint: disable=protected-access or lobj.config.equation_of_state_options != vobj.config.equation_of_state_options ): @@ -115,7 +118,7 @@ def phase_equil(b, phase_pair): # Equilibrium temperature def rule_teq(b): return ( - b._teq[phase_pair] + b._teq[phase_pair] # pylint: disable=protected-access - b.temperature - s[v_phase] * t_units + s[l_phase] * t_units @@ -209,18 +212,26 @@ def rule_cubic_slack_complementarity(b, p): @staticmethod def calculate_scaling_factors(b, phase_pair): + """ + Method to calculate scaling factors for phase equilibrium + """ suffix = "_" + phase_pair[0] + "_" + phase_pair[1] sf_T = iscale.get_scaling_factor(b.temperature, default=1, warning=True) try: teq_cons = getattr(b, "_teq_constraint" + suffix) - iscale.set_scaling_factor(b._teq[phase_pair], sf_T) + iscale.set_scaling_factor( + b._teq[phase_pair], sf_T + ) # pylint: disable=protected-access iscale.constraint_scaling_transform(teq_cons, sf_T, overwrite=False) except AttributeError: pass @staticmethod def calculate_teq(blk, pp): + """ + Method to calculate initial guess for equilibrium temperature + """ # --------------------------------------------------------------------- # If present, initialize bubble and dew point calculations, and # equilibrium temperature _teq @@ -257,7 +268,7 @@ def calculate_teq(blk, pp): else: t2 = t1 - blk._teq[pp].set_value(t2) + blk._teq[pp].set_value(t2) # pylint: disable=protected-access # --------------------------------------------------------------------- # Initialize sV and sL slacks @@ -269,6 +280,9 @@ def calculate_teq(blk, pp): @staticmethod def phase_equil_initialization(b, phase_pair): + """ + Method to initialize phase equilibrium + """ suffix = "_" + phase_pair[0] + "_" + phase_pair[1] for c in b.component_objects(Constraint): @@ -287,12 +301,20 @@ def _calculate_temperature_slacks(b, phase_pair, liquid_phase, vapor_phase): s = getattr(b, "s" + suffix) - if value(b._teq[phase_pair]) > value(b.temperature): - s[vapor_phase].set_value(value(b._teq[phase_pair] - b.temperature)) + if value(b._teq[phase_pair]) > value( + b.temperature + ): # pylint: disable=protected-access + s[vapor_phase].set_value( + value(b._teq[phase_pair] - b.temperature) + ) # pylint: disable=protected-access s[liquid_phase].set_value(EPS_INIT) - elif value(b._teq[phase_pair]) < value(b.temperature): + elif value(b._teq[phase_pair]) < value( + b.temperature + ): # pylint: disable=protected-access s[vapor_phase].set_value(EPS_INIT) - s[liquid_phase].set_value(value(b.temperature - b._teq[phase_pair])) + s[liquid_phase].set_value( + value(b.temperature - b._teq[phase_pair]) + ) # pylint: disable=protected-access else: s[vapor_phase].set_value(EPS_INIT) s[liquid_phase].set_value(EPS_INIT) From 47c29bb8f7814ab2f4df8a39698c3331404b157a Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 21 May 2024 14:36:16 -0400 Subject: [PATCH 19/33] Typo --- .../properties/modular_properties/phase_equil/bubble_dew.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py index 6cfa19fc4a..a3ea324894 100644 --- a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py +++ b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py @@ -39,7 +39,7 @@ class IdealBubbleDew: @staticmethod def temperature_bubble(b): """ - Rule for calculating bubble temeprature + Rule for calculating bubble temperature """ _non_vle_phase_check(b) try: From 8e424cf598bd702c18cece42680ed5a5f356bbbd Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 21 May 2024 14:52:01 -0400 Subject: [PATCH 20/33] More pylinting --- .../modular_properties/base/utility.py | 5 +- .../properties/modular_properties/eos/ceos.py | 1 + .../phase_equil/bubble_dew.py | 52 ++++++++++++++----- .../phase_equil/smooth_VLE_2.py | 27 ++++------ 4 files changed, 52 insertions(+), 33 deletions(-) diff --git a/idaes/models/properties/modular_properties/base/utility.py b/idaes/models/properties/modular_properties/base/utility.py index 3006ea823c..f3424adf19 100644 --- a/idaes/models/properties/modular_properties/base/utility.py +++ b/idaes/models/properties/modular_properties/base/utility.py @@ -265,9 +265,8 @@ def get_concentration_term(blk, r_idx, log=False): conc_form = cfg.inherent_reactions[r_idx].concentration_form state = blk - if ( - hasattr(state.params, "_electrolyte") and state.params._electrolyte - ): # pylint: disable=protected-access + # pylint: disable-next=protected-access + if hasattr(state.params, "_electrolyte") and state.params._electrolyte: sub = "_true" else: sub = "" diff --git a/idaes/models/properties/modular_properties/eos/ceos.py b/idaes/models/properties/modular_properties/eos/ceos.py index 587568dccc..7128ba2021 100644 --- a/idaes/models/properties/modular_properties/eos/ceos.py +++ b/idaes/models/properties/modular_properties/eos/ceos.py @@ -17,6 +17,7 @@ """ # TODO: Pylint complains about variables with _x names as they are built by other classes # pylint: disable=protected-access +# pylint: disable=missing-function-docstring from enum import Enum from copy import deepcopy diff --git a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py index a3ea324894..ac7349d252 100644 --- a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py +++ b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py @@ -514,6 +514,9 @@ class LogBubbleDew: # Bubble temperature methods @staticmethod def temperature_bubble(b): + """ + Method for constructing bubble temperature constraint + """ try: def rule_bubble_temp(b, p1, p2, j): @@ -522,7 +525,7 @@ def rule_bubble_temp(b, p1, p2, j): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, (p1, p2)) @@ -557,7 +560,7 @@ def rule_mole_frac_bubble_temp(b, p1, p2): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, (p1, p2)) @@ -579,6 +582,9 @@ def rule_mole_frac_bubble_temp(b, p1, p2): @staticmethod def scale_temperature_bubble(b, overwrite=True): + """ + Method for scaling bubble temperature + """ sf_mf = iscale.get_scaling_factor(b.mole_frac_comp, default=1e3, warning=True) for pp in b.params._pe_pairs: @@ -587,7 +593,7 @@ def scale_temperature_bubble(b, overwrite=True): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: @@ -604,6 +610,9 @@ def scale_temperature_bubble(b, overwrite=True): # Dew temperature methods @staticmethod def temperature_dew(b): + """ + Method for constructing dew temperature constraint + """ try: def rule_dew_temp(b, p1, p2, j): @@ -613,7 +622,7 @@ def rule_dew_temp(b, p1, p2, j): vl_comps, henry_comps, l_only_comps, - v_only_comps, + _, ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: @@ -648,7 +657,7 @@ def rule_mole_frac_dew_temp(b, p1, p2): vl_comps, henry_comps, l_only_comps, - v_only_comps, + _, ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: @@ -669,6 +678,9 @@ def rule_mole_frac_dew_temp(b, p1, p2): @staticmethod def scale_temperature_dew(b, overwrite=True): + """ + Method for scaling dew temperature + """ sf_mf = iscale.get_scaling_factor(b.mole_frac_comp, default=1e3, warning=True) for pp in b.params._pe_pairs: @@ -677,7 +689,7 @@ def scale_temperature_dew(b, overwrite=True): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: @@ -694,6 +706,9 @@ def scale_temperature_dew(b, overwrite=True): # Bubble pressure methods @staticmethod def pressure_bubble(b): + """ + Method for constructing bubble pressure + """ try: def rule_bubble_press(b, p1, p2, j): @@ -702,7 +717,7 @@ def rule_bubble_press(b, p1, p2, j): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, (p1, p2)) @@ -737,7 +752,7 @@ def rule_mole_frac_bubble_press(b, p1, p2): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, (p1, p2)) @@ -759,6 +774,9 @@ def rule_mole_frac_bubble_press(b, p1, p2): @staticmethod def scale_pressure_bubble(b, overwrite=True): + """ + Method for scaling bubble pressure + """ sf_mf = iscale.get_scaling_factor(b.mole_frac_comp, default=1e3, warning=True) for pp in b.params._pe_pairs: @@ -767,7 +785,7 @@ def scale_pressure_bubble(b, overwrite=True): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: @@ -784,6 +802,9 @@ def scale_pressure_bubble(b, overwrite=True): # Dew pressure methods @staticmethod def pressure_dew(b): + """ + Method constructing dew pressure constraints + """ try: def rule_dew_press(b, p1, p2, j): @@ -792,7 +813,7 @@ def rule_dew_press(b, p1, p2, j): v_phase, vl_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(b, (p1, p2)) @@ -828,7 +849,7 @@ def rule_mole_frac_dew_press(b, p1, p2): vl_comps, henry_comps, l_only_comps, - v_only_comps, + _, ) = identify_VL_component_list(b, (p1, p2)) if l_phase is None or v_phase is None: @@ -849,15 +870,18 @@ def rule_mole_frac_dew_press(b, p1, p2): @staticmethod def scale_pressure_dew(b, overwrite=True): + """ + Method for scaling dew pressure + """ sf_mf = iscale.get_scaling_factor(b.mole_frac_comp, default=1e3, warning=True) for pp in b.params._pe_pairs: ( l_phase, v_phase, - vl_comps, - henry_comps, - l_only_comps, + _, + _, + _, v_only_comps, ) = identify_VL_component_list(b, pp) if l_phase is None or v_phase is None: diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index ac52083781..a4e101863f 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -220,9 +220,8 @@ def calculate_scaling_factors(b, phase_pair): try: teq_cons = getattr(b, "_teq_constraint" + suffix) - iscale.set_scaling_factor( - b._teq[phase_pair], sf_T - ) # pylint: disable=protected-access + # pylint: disable-next=protected-access + iscale.set_scaling_factor(b._teq[phase_pair], sf_T) iscale.constraint_scaling_transform(teq_cons, sf_T, overwrite=False) except AttributeError: pass @@ -242,7 +241,7 @@ def calculate_teq(blk, pp): vapor_phase, raoult_comps, henry_comps, - l_only_comps, + _, v_only_comps, ) = identify_VL_component_list(blk, pp) @@ -301,20 +300,16 @@ def _calculate_temperature_slacks(b, phase_pair, liquid_phase, vapor_phase): s = getattr(b, "s" + suffix) - if value(b._teq[phase_pair]) > value( - b.temperature - ): # pylint: disable=protected-access - s[vapor_phase].set_value( - value(b._teq[phase_pair] - b.temperature) - ) # pylint: disable=protected-access + # pylint: disable-next=protected-access + if value(b._teq[phase_pair]) > value(b.temperature): + # pylint: disable-next=protected-access + s[vapor_phase].set_value(value(b._teq[phase_pair] - b.temperature)) s[liquid_phase].set_value(EPS_INIT) - elif value(b._teq[phase_pair]) < value( - b.temperature - ): # pylint: disable=protected-access + # pylint: disable-next=protected-access + elif value(b._teq[phase_pair]) < value(b.temperature): s[vapor_phase].set_value(EPS_INIT) - s[liquid_phase].set_value( - value(b.temperature - b._teq[phase_pair]) - ) # pylint: disable=protected-access + # pylint: disable-next=protected-access + s[liquid_phase].set_value(value(b.temperature - b._teq[phase_pair])) else: s[vapor_phase].set_value(EPS_INIT) s[liquid_phase].set_value(EPS_INIT) From 31fba136671a7d1531f25f417a577434de7d6007 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 21 May 2024 15:26:31 -0400 Subject: [PATCH 21/33] More pylint --- .../modular_properties/phase_equil/bubble_dew.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py index ac7349d252..f6d75d1931 100644 --- a/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py +++ b/idaes/models/properties/modular_properties/phase_equil/bubble_dew.py @@ -591,8 +591,8 @@ def scale_temperature_bubble(b, overwrite=True): ( l_phase, v_phase, - vl_comps, - henry_comps, + _, + _, _, v_only_comps, ) = identify_VL_component_list(b, pp) @@ -687,8 +687,8 @@ def scale_temperature_dew(b, overwrite=True): ( l_phase, v_phase, - vl_comps, - henry_comps, + _, + _, _, v_only_comps, ) = identify_VL_component_list(b, pp) @@ -783,8 +783,8 @@ def scale_pressure_bubble(b, overwrite=True): ( l_phase, v_phase, - vl_comps, - henry_comps, + _, + _, _, v_only_comps, ) = identify_VL_component_list(b, pp) From b27706a098acb1239d8407f0a5feee539e7b8305 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 21 May 2024 15:37:04 -0400 Subject: [PATCH 22/33] Clean up debugging code --- .../base/tests/test_generic_property_integration.py | 1 - 1 file changed, 1 deletion(-) diff --git a/idaes/models/properties/modular_properties/base/tests/test_generic_property_integration.py b/idaes/models/properties/modular_properties/base/tests/test_generic_property_integration.py index f3b90da83e..1968f08176 100644 --- a/idaes/models/properties/modular_properties/base/tests/test_generic_property_integration.py +++ b/idaes/models/properties/modular_properties/base/tests/test_generic_property_integration.py @@ -217,7 +217,6 @@ def test_heater_w_inherent_rxns_comp_phase(self, frame): rel=1e-5, ) - frame.fs.H101.outlet.mole_frac_comp.display() assert value(frame.fs.H101.outlet.mole_frac_comp[0, "a"]) == pytest.approx( 1 / 6, rel=1e-5 ) From 87713238b94aabbf1ecf53d53c23717d0f854823 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 21 May 2024 16:03:56 -0400 Subject: [PATCH 23/33] Fixing final test --- .../unit_models/tests/test_heat_exchanger.py | 308 ++++++++++-------- 1 file changed, 171 insertions(+), 137 deletions(-) diff --git a/idaes/models/unit_models/tests/test_heat_exchanger.py b/idaes/models/unit_models/tests/test_heat_exchanger.py index 16ddfaed1e..81d28580a9 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger.py @@ -15,6 +15,7 @@ Author: John Eslick """ +from copy import deepcopy import pytest import pandas @@ -1905,158 +1906,155 @@ def test_initialization_error(self, btx): btx.fs.unit.initialize() -class TestInitializersModular: - @pytest.fixture - def model(self): - m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - - # As we lack other example prop packs with units, take the generic - # BT-PR package and change the base units - configuration2 = { - # Specifying components - "components": { - "benzene": { - "type": Component, - "enth_mol_ig_comp": RPP, - "entr_mol_ig_comp": RPP, - "pressure_sat_comp": RPP, - "phase_equilibrium_form": {("Vap", "Liq"): log_fugacity}, - "parameter_data": { - "mw": (78.1136e-3, pyunits.kg / pyunits.mol), # [1] - "pressure_crit": (48.9e5, pyunits.Pa), # [1] - "temperature_crit": (562.2, pyunits.K), # [1] - "omega": 0.212, # [1] - "cp_mol_ig_comp_coeff": { - "A": (-3.392e1, pyunits.J / pyunits.mol / pyunits.K), # [1] - "B": (4.739e-1, pyunits.J / pyunits.mol / pyunits.K**2), - "C": (-3.017e-4, pyunits.J / pyunits.mol / pyunits.K**3), - "D": (7.130e-8, pyunits.J / pyunits.mol / pyunits.K**4), - }, - "enth_mol_form_vap_comp_ref": ( - 82.9e3, - pyunits.J / pyunits.mol, - ), # [3] - "entr_mol_form_vap_comp_ref": ( - -269, - pyunits.J / pyunits.mol / pyunits.K, - ), # [3] - "pressure_sat_comp_coeff": { - "A": (-6.98273, None), # [1] - "B": (1.33213, None), - "C": (-2.62863, None), - "D": (-3.33399, None), - }, - }, +# As we lack other example prop packs with units, take the generic +# BT-PR package and change the base units +configuration2 = { + # Specifying components + "components": { + "benzene": { + "type": Component, + "enth_mol_ig_comp": RPP, + "entr_mol_ig_comp": RPP, + "pressure_sat_comp": RPP, + "phase_equilibrium_form": {("Vap", "Liq"): log_fugacity}, + "parameter_data": { + "mw": (78.1136e-3, pyunits.kg / pyunits.mol), # [1] + "pressure_crit": (48.9e5, pyunits.Pa), # [1] + "temperature_crit": (562.2, pyunits.K), # [1] + "omega": 0.212, # [1] + "cp_mol_ig_comp_coeff": { + "A": (-3.392e1, pyunits.J / pyunits.mol / pyunits.K), # [1] + "B": (4.739e-1, pyunits.J / pyunits.mol / pyunits.K**2), + "C": (-3.017e-4, pyunits.J / pyunits.mol / pyunits.K**3), + "D": (7.130e-8, pyunits.J / pyunits.mol / pyunits.K**4), }, - "toluene": { - "type": Component, - "enth_mol_ig_comp": RPP, - "entr_mol_ig_comp": RPP, - "pressure_sat_comp": RPP, - "phase_equilibrium_form": {("Vap", "Liq"): log_fugacity}, - "parameter_data": { - "mw": (92.1405e-3, pyunits.kg / pyunits.mol), # [1] - "pressure_crit": (41e5, pyunits.Pa), # [1] - "temperature_crit": (591.8, pyunits.K), # [1] - "omega": 0.263, # [1] - "cp_mol_ig_comp_coeff": { - "A": (-2.435e1, pyunits.J / pyunits.mol / pyunits.K), # [1] - "B": (5.125e-1, pyunits.J / pyunits.mol / pyunits.K**2), - "C": (-2.765e-4, pyunits.J / pyunits.mol / pyunits.K**3), - "D": (4.911e-8, pyunits.J / pyunits.mol / pyunits.K**4), - }, - "enth_mol_form_vap_comp_ref": ( - 50.1e3, - pyunits.J / pyunits.mol, - ), # [3] - "entr_mol_form_vap_comp_ref": ( - -321, - pyunits.J / pyunits.mol / pyunits.K, - ), # [3] - "pressure_sat_comp_coeff": { - "A": (-7.28607, None), # [1] - "B": (1.38091, None), - "C": (-2.83433, None), - "D": (-2.79168, None), - }, - }, + "enth_mol_form_vap_comp_ref": ( + 82.9e3, + pyunits.J / pyunits.mol, + ), # [3] + "entr_mol_form_vap_comp_ref": ( + -269, + pyunits.J / pyunits.mol / pyunits.K, + ), # [3] + "pressure_sat_comp_coeff": { + "A": (-6.98273, None), # [1] + "B": (1.33213, None), + "C": (-2.62863, None), + "D": (-3.33399, None), }, }, - # Specifying phases - "phases": { - "Liq": { - "type": LiquidPhase, - "equation_of_state": Cubic, - "equation_of_state_options": {"type": CubicType.PR}, + }, + "toluene": { + "type": Component, + "enth_mol_ig_comp": RPP, + "entr_mol_ig_comp": RPP, + "pressure_sat_comp": RPP, + "phase_equilibrium_form": {("Vap", "Liq"): log_fugacity}, + "parameter_data": { + "mw": (92.1405e-3, pyunits.kg / pyunits.mol), # [1] + "pressure_crit": (41e5, pyunits.Pa), # [1] + "temperature_crit": (591.8, pyunits.K), # [1] + "omega": 0.263, # [1] + "cp_mol_ig_comp_coeff": { + "A": (-2.435e1, pyunits.J / pyunits.mol / pyunits.K), # [1] + "B": (5.125e-1, pyunits.J / pyunits.mol / pyunits.K**2), + "C": (-2.765e-4, pyunits.J / pyunits.mol / pyunits.K**3), + "D": (4.911e-8, pyunits.J / pyunits.mol / pyunits.K**4), }, - "Vap": { - "type": VaporPhase, - "equation_of_state": Cubic, - "equation_of_state_options": {"type": CubicType.PR}, + "enth_mol_form_vap_comp_ref": ( + 50.1e3, + pyunits.J / pyunits.mol, + ), # [3] + "entr_mol_form_vap_comp_ref": ( + -321, + pyunits.J / pyunits.mol / pyunits.K, + ), # [3] + "pressure_sat_comp_coeff": { + "A": (-7.28607, None), # [1] + "B": (1.38091, None), + "C": (-2.83433, None), + "D": (-2.79168, None), }, }, - # Set base units of measurement - "base_units": { - "time": pyunits.s, - "length": pyunits.m, - "mass": pyunits.t, - "amount": pyunits.mol, - "temperature": pyunits.degR, - }, - # Specifying state definition - "state_definition": FTPx, - "state_bounds": { - "flow_mol": (0, 100, 1000, pyunits.mol / pyunits.s), - "temperature": (273.15, 300, 500, pyunits.K), - "pressure": (5e4, 1e5, 1e6, pyunits.Pa), - }, - "pressure_ref": (101325, pyunits.Pa), - "temperature_ref": (298.15, pyunits.K), - # Defining phase equilibria - "phases_in_equilibrium": [("Vap", "Liq")], - "phase_equilibrium_state": {("Vap", "Liq"): SmoothVLE}, - "bubble_dew_method": LogBubbleDew, - "parameter_data": { - "PR_kappa": { - ("benzene", "benzene"): 0.000, - ("benzene", "toluene"): 0.000, - ("toluene", "benzene"): 0.000, - ("toluene", "toluene"): 0.000, - } - }, + }, + }, + # Specifying phases + "phases": { + "Liq": { + "type": LiquidPhase, + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + "Vap": { + "type": VaporPhase, + "equation_of_state": Cubic, + "equation_of_state_options": {"type": CubicType.PR}, + }, + }, + # Set base units of measurement + "base_units": { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.t, + "amount": pyunits.mol, + "temperature": pyunits.degR, + }, + # Specifying state definition + "state_definition": FTPx, + "state_bounds": { + "flow_mol": (0, 100, 1000, pyunits.mol / pyunits.s), + "temperature": (273.15, 300, 500, pyunits.K), + "pressure": (5e4, 1e5, 1e6, pyunits.Pa), + }, + "pressure_ref": (101325, pyunits.Pa), + "temperature_ref": (298.15, pyunits.K), + # Defining phase equilibria + "phases_in_equilibrium": [("Vap", "Liq")], + "phase_equilibrium_state": {("Vap", "Liq"): SmoothVLE}, + "bubble_dew_method": LogBubbleDew, + "parameter_data": { + "PR_kappa": { + ("benzene", "benzene"): 0.000, + ("benzene", "toluene"): 0.000, + ("toluene", "benzene"): 0.000, + ("toluene", "toluene"): 0.000, } + }, +} - m.fs.properties = GenericParameterBlock(**configuration) - m.fs.properties2 = GenericParameterBlock(**configuration2) - m.fs.unit = HeatExchanger( - hot_side={"property_package": m.fs.properties}, - cold_side={"property_package": m.fs.properties2}, +class TestInitializersModular: + @pytest.mark.integration + def test_hx0d_initializer(self): + model = ConcreteModel() + model.fs = FlowsheetBlock(dynamic=False) + + model.fs.properties = GenericParameterBlock(**configuration) + model.fs.properties2 = GenericParameterBlock(**configuration2) + + model.fs.unit = HeatExchanger( + hot_side={"property_package": model.fs.properties}, + cold_side={"property_package": model.fs.properties2}, flow_pattern=HeatExchangerFlowPattern.cocurrent, ) - m.fs.unit.hot_side_inlet.flow_mol[0].fix(5) # mol/s - m.fs.unit.hot_side_inlet.temperature[0].fix(365) # K - m.fs.unit.hot_side_inlet.pressure[0].fix(101325) # Pa - m.fs.unit.hot_side_inlet.mole_frac_comp[0, "benzene"].fix(0.5) - m.fs.unit.hot_side_inlet.mole_frac_comp[0, "toluene"].fix(0.5) - - m.fs.unit.cold_side_inlet.flow_mol[0].fix(1) # mol/s - m.fs.unit.cold_side_inlet.temperature[0].fix(540) # degR - m.fs.unit.cold_side_inlet.pressure[0].fix(101.325) # kPa - m.fs.unit.cold_side_inlet.mole_frac_comp[0, "benzene"].fix(0.5) - m.fs.unit.cold_side_inlet.mole_frac_comp[0, "toluene"].fix(0.5) + model.fs.unit.hot_side_inlet.flow_mol[0].fix(5) # mol/s + model.fs.unit.hot_side_inlet.temperature[0].fix(365) # K + model.fs.unit.hot_side_inlet.pressure[0].fix(101325) # Pa + model.fs.unit.hot_side_inlet.mole_frac_comp[0, "benzene"].fix(0.5) + model.fs.unit.hot_side_inlet.mole_frac_comp[0, "toluene"].fix(0.5) - m.fs.unit.area.fix(1) - m.fs.unit.overall_heat_transfer_coefficient.fix(100) + model.fs.unit.cold_side_inlet.flow_mol[0].fix(1) # mol/s + model.fs.unit.cold_side_inlet.temperature[0].fix(540) # degR + model.fs.unit.cold_side_inlet.pressure[0].fix(101.325) # kPa + model.fs.unit.cold_side_inlet.mole_frac_comp[0, "benzene"].fix(0.5) + model.fs.unit.cold_side_inlet.mole_frac_comp[0, "toluene"].fix(0.5) - m.fs.unit.cold_side.scaling_factor_pressure = 1 + model.fs.unit.area.fix(1) + model.fs.unit.overall_heat_transfer_coefficient.fix(100) - return m + model.fs.unit.cold_side.scaling_factor_pressure = 1 - @pytest.mark.integration - def test_hx0d_initializer(self, model): initializer = HX0DInitializer() initializer.initialize(model.fs.unit) @@ -2083,7 +2081,43 @@ def test_hx0d_initializer(self, model): ) @pytest.mark.integration - def test_block_triangularization(self, model): + def test_block_triangularization( + self, + ): + # Trying to get this to work with SmoothVLE2 is challenging, and + # not necessary for this particular test + new_config = deepcopy(configuration) + new_config["phase_equilibrium_state"] = {("Vap", "Liq"): SmoothVLE} + + model = ConcreteModel() + model.fs = FlowsheetBlock(dynamic=False) + + model.fs.properties = GenericParameterBlock(**new_config) + model.fs.properties2 = GenericParameterBlock(**configuration2) + + model.fs.unit = HeatExchanger( + hot_side={"property_package": model.fs.properties}, + cold_side={"property_package": model.fs.properties2}, + flow_pattern=HeatExchangerFlowPattern.cocurrent, + ) + + model.fs.unit.hot_side_inlet.flow_mol[0].fix(5) # mol/s + model.fs.unit.hot_side_inlet.temperature[0].fix(365) # K + model.fs.unit.hot_side_inlet.pressure[0].fix(101325) # Pa + model.fs.unit.hot_side_inlet.mole_frac_comp[0, "benzene"].fix(0.5) + model.fs.unit.hot_side_inlet.mole_frac_comp[0, "toluene"].fix(0.5) + + model.fs.unit.cold_side_inlet.flow_mol[0].fix(1) # mol/s + model.fs.unit.cold_side_inlet.temperature[0].fix(540) # degR + model.fs.unit.cold_side_inlet.pressure[0].fix(101.325) # kPa + model.fs.unit.cold_side_inlet.mole_frac_comp[0, "benzene"].fix(0.5) + model.fs.unit.cold_side_inlet.mole_frac_comp[0, "toluene"].fix(0.5) + + model.fs.unit.area.fix(1) + model.fs.unit.overall_heat_transfer_coefficient.fix(100) + + model.fs.unit.cold_side.scaling_factor_pressure = 1 + initializer = BlockTriangularizationInitializer( constraint_tolerance=2e-5, ) From 8dd941ba52d64d6354a06bb25d0faae1aef7a75e Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Fri, 24 May 2024 14:15:48 -0400 Subject: [PATCH 24/33] Some clean up --- idaes/models/properties/modular_properties/base/utility.py | 6 ++++++ .../modular_properties/phase_equil/smooth_VLE_2.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/idaes/models/properties/modular_properties/base/utility.py b/idaes/models/properties/modular_properties/base/utility.py index f3424adf19..5dcaf9eeae 100644 --- a/idaes/models/properties/modular_properties/base/utility.py +++ b/idaes/models/properties/modular_properties/base/utility.py @@ -592,6 +592,12 @@ def estimate_Pdew(blk, raoult_comps, henry_comps, liquid_phase): Estimated dew point pressure as a float. """ + # Safety catch for cases where Psat or Henry's constant might be 0 + # Not sure if this is meaningful, but if this is true then mathematically Pdew = 0 + if any(value(blk.pressure_sat_comp[j]) == 0 for j in raoult_comps) or any( + value(blk.henry[liquid_phase, j]) == 0 for j in henry_comps + ): + return 0 return value( 1 / ( diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index a4e101863f..6836c7fb6b 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -106,6 +106,10 @@ def phase_equil(b, phase_pair): vl_phase_set = Set(initialize=[phase_pair[0], phase_pair[1]]) b.add_component("_vle_set" + suffix, vl_phase_set) + # Determination of "correct" units for slacks is challenging, as they are used to complement + # both flow and temperature. From a scaling perspective, the value (and thus units?) are + # based on the magnitude larger of these two, and thus hard to know a priori. + # This also applies to epsilon. s = Var( vl_phase_set, initialize=EPS_INIT, From f43e3e03e82c3eafaadad592d9c07459cc1c6904 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 27 May 2024 13:48:20 -0400 Subject: [PATCH 25/33] Using logspace --- .../modular_properties/examples/tests/test_BT_PR.py | 8 ++++---- .../examples/tests/test_BT_PR_legacy_SmoothVLE.py | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py index b39b7c0213..16d4fdca93 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py @@ -14,9 +14,10 @@ Author: Andrew Lee """ - import pytest +from numpy import logspace + from pyomo.util.check_units import assert_units_consistent from pyomo.environ import assert_optimal_termination, ConcreteModel, Objective, value @@ -79,14 +80,13 @@ def test_T_sweep(self, m): m.fs.obj = Objective(expr=(m.fs.state[1].temperature - 510) ** 2) m.fs.state[1].temperature.setub(600) - for logP in [9.5, 10, 10.5, 11, 11.5, 12]: - m.fs.obj.deactivate() + for P in logspace(4.8, 5.9, 8): m.fs.state[1].flow_mol.fix(100) m.fs.state[1].mole_frac_comp["benzene"].fix(0.5) m.fs.state[1].mole_frac_comp["toluene"].fix(0.5) m.fs.state[1].temperature.fix(300) - m.fs.state[1].pressure.fix(10 ** (0.5 * logP)) + m.fs.state[1].pressure.fix(P) # For optimization sweep, use a large eps to avoid getting stuck at # bubble and dew points diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_legacy_SmoothVLE.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_legacy_SmoothVLE.py index ac6e48ec1f..f25035437a 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_legacy_SmoothVLE.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR_legacy_SmoothVLE.py @@ -16,6 +16,8 @@ import pytest +from numpy import logspace + from pyomo.util.check_units import assert_units_consistent from pyomo.environ import ( check_optimal_termination, @@ -210,14 +212,14 @@ def test_T_sweep(self, m): m.fs.obj = Objective(expr=(m.fs.state[1].temperature - 510) ** 2) m.fs.state[1].temperature.setub(600) - for logP in [9.5, 10, 10.5, 11, 11.5, 12]: + for P in logspace(4.8, 5.9, 8): m.fs.obj.deactivate() m.fs.state[1].flow_mol.fix(100) m.fs.state[1].mole_frac_comp["benzene"].fix(0.5) m.fs.state[1].mole_frac_comp["toluene"].fix(0.5) m.fs.state[1].temperature.fix(300) - m.fs.state[1].pressure.fix(10 ** (0.5 * logP)) + m.fs.state[1].pressure.fix(P) m.fs.state.initialize() From 7a0375b0aab854d3a573c0ed251d0c5fcc2766d7 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 27 May 2024 13:48:39 -0400 Subject: [PATCH 26/33] Adding initial docs --- .../general/pe/smooth_flash.rst | 7 ++- .../general/pe/smooth_vle2.rst | 55 +++++++++++++++++++ .../general/phase_equilibrium.rst | 1 + 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 docs/explanations/components/property_package/general/pe/smooth_vle2.rst diff --git a/docs/explanations/components/property_package/general/pe/smooth_flash.rst b/docs/explanations/components/property_package/general/pe/smooth_flash.rst index 540429ba27..95ea4a9d36 100644 --- a/docs/explanations/components/property_package/general/pe/smooth_flash.rst +++ b/docs/explanations/components/property_package/general/pe/smooth_flash.rst @@ -1,9 +1,12 @@ -Smooth Vapor-Liquid Equilibrium Formulation (``smooth_VLE``) -============================================================ +Smooth Vapor-Liquid Equilibrium Formulation (SmoothVLE) +======================================================= .. contents:: Contents :depth: 2 +.. note:: + For property packages using cubic Equations of State, there is an alternative :ref:`SmoothVLE2 ` class that may give better performance. + Source ------ diff --git a/docs/explanations/components/property_package/general/pe/smooth_vle2.rst b/docs/explanations/components/property_package/general/pe/smooth_vle2.rst new file mode 100644 index 0000000000..22fda93d29 --- /dev/null +++ b/docs/explanations/components/property_package/general/pe/smooth_vle2.rst @@ -0,0 +1,55 @@ +Cubic Smooth Vapor-Liquid Equilibrium Formulation (SmoothVLE2) +============================================================== + +.. contents:: Contents + :depth: 2 + +.. note:: + This formulation for vapor-liquid equilibrium is only valid if using a cubic Equation of State. For other equations of state, use :ref:`SmoothVLE `. + +Source +------ + +Dabadghao, V., Ghouse, J., Eslick, J., Lee, A., Burgard, A., Miller, D., and Biegler, L., A Complementarity-based Vapor-Liquid Equilibrium Formulation for Equation-Oriented Simulation and Optimization, AIChE Journal, 2023, Volume 69(4), e18029. https://doi.org/10.1002/aic.18029 + +Introduction +------------ + +Typically, equilibrium calculations are only used when the user knows the current state is within the two-phase envelope. For simulation only studies, the user may know a priori the condition of the stream but when the same set of equations are used for optimization, there is a high probability that the specifications can transcend the phase envelope. In these situations, the equilibrium calculations become trivial, thus it is necessary to find a formulation that has non-trivial solutions at all states. + +To address this, the cubic smooth vapor-liquid equilibrium (VLE) formulation always solves the equilibrium calculations at a condition where a valid two-phase solution exists. In situations where only a single phase is present, the phase equilibrium is solved at the either the bubble or dew point, where the non-existent phase exists but in negligible amounts. In this way, a non-trivial solution is guaranteed but still gives near-zero material in the non-existent phase in the single phase regions. Rather than explicitly calculate the bubble and dew points (as is done in the non-cubic formulation), this formulation leverages properties of the cubic equation of state to identify the "equilibrium temperature". + +Formulation +----------- + +.. note:: + For the full derivation of the cubic smooth VLE formulation, see the reference above. + +.. note:: + For consistency of naming between the cubic and non-cubic formulations, :math:`\bar{T}` is referred to as :math:`T_{eq}` in this document and the resulting model. + +The approach used by the smooth VLE formulation is to define an "equilibrium temperature" (:math:`T_{eq}`) at which the equilibrium calculations will be performed. The equilibrium temperature is defined such that: + +.. math:: T = T_{eq} - s_{vap} + s_{liq} + +where :math:`T` is the actual state temperature, and :math:`s_{liq}` and :math:`s_{vap}` are non-negative slack variables. For systems existing the the liquid-only region, :math:`s_{liq}` will be non-zero whilst :math:`s_{vap}=0` (indicating that the system is below the bubble point and thus :math:`T_{eq}>T`). Similarly, for systems in the vapor-only region, :math:`s_{vap}` will be non-zero whilst :math:`s_{liq}=0`. Finally, in the two-phase region, :math:`s_{liq}=s_{vap}=0`, indicating that :math:`T_{eq}=T`. + +In order to determine the values of :math:`s_{liq}` and :math:`s_{vap}`, the following complementarity constraints are written: + +.. math:: 0 = \min(s_{liq}, F_{liq}) +.. math:: 0 = \min(s_{vap}, F_{vap}) + +where :math:`F_{p}` is the flow rate of each phase :math:`p`. That is, for each phase (liquid and vapor), if thee is any flowrate associated with that phase (i.e., the phase exists), its slack variable must be equal to zero. + +Additionally, the follow complementarities are written to constraint the roots of the cubic equation of state. + +.. math:: 0 = \min(gp_{liq}, F_{liq}) +.. math:: 0 = \min(gn_{vap}, F_{vap}) + +where :math:`gp_p` and :math:`gn_p` are another pair of non-negative slack variables associated with each phase :math:`p`. These slack variables are defined such that: + +.. math:: f''(Z_p) = gp_{p} - gn_{p} + +where :math:`f''(Z_p)` is the second derivative of the cubic equation of state written in terms of the compressibility factor :math:`Z_p` for each phase :math:`p`. + + diff --git a/docs/explanations/components/property_package/general/phase_equilibrium.rst b/docs/explanations/components/property_package/general/phase_equilibrium.rst index ad1e45979d..e21409580f 100644 --- a/docs/explanations/components/property_package/general/phase_equilibrium.rst +++ b/docs/explanations/components/property_package/general/phase_equilibrium.rst @@ -40,6 +40,7 @@ Phase Equilibrium State Libraries :maxdepth: 1 pe/smooth_flash + pe/smooth_vle2 Necessary Properties -------------------- From b563460bb80ecad1c734b10e22edd5cf906e5310 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 27 May 2024 14:52:34 -0400 Subject: [PATCH 27/33] Documenting epsilons --- .../general/pe/smooth_vle2.rst | 20 +++++++++++++++---- .../examples/tests/test_BT_PR.py | 8 ++++---- .../phase_equil/smooth_VLE_2.py | 18 ++++++++--------- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/docs/explanations/components/property_package/general/pe/smooth_vle2.rst b/docs/explanations/components/property_package/general/pe/smooth_vle2.rst index 22fda93d29..1516eada69 100644 --- a/docs/explanations/components/property_package/general/pe/smooth_vle2.rst +++ b/docs/explanations/components/property_package/general/pe/smooth_vle2.rst @@ -43,13 +43,25 @@ where :math:`F_{p}` is the flow rate of each phase :math:`p`. That is, for each Additionally, the follow complementarities are written to constraint the roots of the cubic equation of state. -.. math:: 0 = \min(gp_{liq}, F_{liq}) -.. math:: 0 = \min(gn_{vap}, F_{vap}) +.. math:: 0 = \min(g^{+}_{liq}, F_{liq}) +.. math:: 0 = \min(g^{-}_{vap}, F_{vap}) -where :math:`gp_p` and :math:`gn_p` are another pair of non-negative slack variables associated with each phase :math:`p`. These slack variables are defined such that: +where :math:`g^{+}_p` and :math:`g^{-}_p` are another pair of non-negative slack variables associated with each phase :math:`p`. These slack variables are defined such that: -.. math:: f''(Z_p) = gp_{p} - gn_{p} +.. math:: f''(Z_p) = g^{+}_{p} - g^{-}_{p} where :math:`f''(Z_p)` is the second derivative of the cubic equation of state written in terms of the compressibility factor :math:`Z_p` for each phase :math:`p`. +Smooth Approximation +'''''''''''''''''''' + +In order to express the minimum operators in a tractable form, these equations are reformulated using the IDAES `smooth_min` function: + +.. math:: \min(a, b) = 0.5{\left[a + b - \sqrt{(a-b)^2 + \epsilon^2}\right]} + +Each complementarity requires a smoothing parameter, named :math:`\epsilon_T` and :math:`\epsilon_Z` for the temperature and cubic root constraints respectively. Within the IDAES model, these are rendered as ``eps_t_phase1_phase2`` and ``eps_z_phase1_phase2``, where ``phase1`` and ``phase2`` are the names assigned to the liquid and vapor phases in the property package (order will depend on the order these are declared). + +The tractability of the VLE problem depends heavily upon the values chosen for :math:`\epsilon_T` and :math:`\epsilon_Z`, with larger values resulting in smoother transitions at the phase boundaries (and thus increased tractability) at the expense of decreased accuracy near these points. It is recommended that users employ a 2-stage approach to solving these problems, starting with a larger value of :math:`\epsilon_T` and :math:`\epsilon_Z` initially to determine which region the solution lies in, followed by a second solve using smaller values to refine the solution. + +As a rule of thumb, the values of :math:`\epsilon_T` and :math:`\epsilon_Z` should be between 2 and 4 orders of magnitude smaller than the largest quantify involved in the smooth maximum operation. This means the value of :math:`\epsilon_T` should be based on the larger of :math:`T` and :math:`F_p`, whilst :math:`\epsilon_Z` should be based on the larger of :math:`f''(Z_p)` and :math:`F_p`. The value of :math:`f''(Z_p)` may be difficult to determine *a priori*, however :math:`F_p` is likely to dominate in most cases unless :math:`F_p` is small or :math:`P` is large. diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py index 16d4fdca93..fb6b14c5db 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py @@ -90,8 +90,8 @@ def test_T_sweep(self, m): # For optimization sweep, use a large eps to avoid getting stuck at # bubble and dew points - m.fs.state[1].eps_1_Vap_Liq.set_value(10) - m.fs.state[1].eps_2_Vap_Liq.set_value(10) + m.fs.state[1].eps_t_Vap_Liq.set_value(10) + m.fs.state[1].eps_z_Vap_Liq.set_value(10) m.fs.state.initialize() @@ -102,8 +102,8 @@ def test_T_sweep(self, m): assert_optimal_termination(results) # Switch to small eps and re-solve to refine result - m.fs.state[1].eps_1_Vap_Liq.set_value(1e-4) - m.fs.state[1].eps_2_Vap_Liq.set_value(1e-4) + m.fs.state[1].eps_t_Vap_Liq.set_value(1e-4) + m.fs.state[1].eps_z_Vap_Liq.set_value(1e-4) results = solver.solve(m) diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 6836c7fb6b..2db8232aaa 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -131,21 +131,21 @@ def rule_teq(b): b.add_component("_teq_constraint" + suffix, Constraint(rule=rule_teq)) - eps1 = Param( + eps_t = Param( default=1e-4, mutable=True, - doc="Smoothing parameter for complementarities", + doc="Smoothing parameter for temperature complementarity", units=f_units, ) - b.add_component("eps_1" + suffix, eps1) + b.add_component("eps_t" + suffix, eps_t) - eps2 = Param( + eps_z = Param( default=1e-4, mutable=True, - doc="Smoothing parameter for complementarities", + doc="Smoothing parameter for cubic root complementarities", units=f_units, ) - b.add_component("eps_2" + suffix, eps2) + b.add_component("eps_z" + suffix, eps_z) gp = Var( vl_phase_set, @@ -168,7 +168,7 @@ def rule_teq(b): def rule_temperature_slack_complementarity(b, p): flow_phase = b.flow_mol_phase[p] - return smooth_min(s[p] * f_units, flow_phase, eps1) == 0 + return smooth_min(s[p] * f_units, flow_phase, eps_t) == 0 b.add_component( "temperature_slack_complementarity" + suffix, @@ -205,9 +205,9 @@ def rule_cubic_root_complementarity(b, p): def rule_cubic_slack_complementarity(b, p): flow_phase = b.flow_mol_phase[p] if b.params.get_phase(p).is_vapor_phase(): - return smooth_min(gn[p] * f_units, flow_phase, eps2) == 0 + return smooth_min(gn[p] * f_units, flow_phase, eps_z) == 0 else: - return smooth_min(gp[p] * f_units, flow_phase, eps2) == 0 + return smooth_min(gp[p] * f_units, flow_phase, eps_z) == 0 b.add_component( "cubic_slack_complementarity" + suffix, From c0a9e1850ad8ee1e89923e6c6a82fecc2f71c24f Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 27 May 2024 15:28:09 -0400 Subject: [PATCH 28/33] Fixing test --- .../phase_equil/tests/test_smooth_VLE_2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py index e349fd81a6..ffb120db29 100644 --- a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py @@ -208,10 +208,10 @@ def test_build(frame): assert isinstance(frame.props[1].cubic_second_derivative_Vap_Liq, Expression) - assert isinstance(frame.props[1].eps_1_Vap_Liq, Param) - assert value(frame.props[1].eps_1_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) - assert isinstance(frame.props[1].eps_2_Vap_Liq, Param) - assert value(frame.props[1].eps_2_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) + assert isinstance(frame.props[1].eps_t_Vap_Liq, Param) + assert value(frame.props[1].eps_t_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) + assert isinstance(frame.props[1].eps_z_Vap_Liq, Param) + assert value(frame.props[1].eps_z_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) assert isinstance(frame.props[1]._teq_constraint_Vap_Liq, Constraint) assert isinstance( From 9d352773d3ba941d5eb8adaff02f9b3981709a84 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 28 May 2024 11:11:25 -0400 Subject: [PATCH 29/33] Adjusting default value of epsilon --- .../examples/tests/test_BT_PR.py | 5 +++++ .../phase_equil/smooth_VLE_2.py | 18 ++++++++---------- .../phase_equil/tests/test_smooth_VLE_2.py | 4 ++-- .../unit_models/tests/test_heat_exchanger.py | 14 ++++++++++++++ .../tests/test_heat_exchanger_1D.py | 11 +++++++++++ idaes/models/unit_models/tests/test_heater.py | 13 +++++++++++++ .../tests/test_shell_and_tube_1D.py | 6 ++++++ 7 files changed, 59 insertions(+), 12 deletions(-) diff --git a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py index fb6b14c5db..ad9ad9afa7 100644 --- a/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/tests/test_BT_PR.py @@ -69,6 +69,11 @@ def m(self): m.fs.state = m.fs.props.build_state_block([1], defined_state=True) + # Set small values of epsilon to get accurate results + # Initialization will handle finding the correct region + m.fs.state[1].eps_t_Vap_Liq.set_value(1e-4) + m.fs.state[1].eps_z_Vap_Liq.set_value(1e-4) + iscale.calculate_scaling_factors(m.fs.props) iscale.calculate_scaling_factors(m.fs.state[1]) return m diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 2db8232aaa..86912741df 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -106,16 +106,12 @@ def phase_equil(b, phase_pair): vl_phase_set = Set(initialize=[phase_pair[0], phase_pair[1]]) b.add_component("_vle_set" + suffix, vl_phase_set) - # Determination of "correct" units for slacks is challenging, as they are used to complement - # both flow and temperature. From a scaling perspective, the value (and thus units?) are - # based on the magnitude larger of these two, and thus hard to know a priori. - # This also applies to epsilon. s = Var( vl_phase_set, initialize=EPS_INIT, bounds=(0, None), doc="Slack variable for equilibrium temperature", - units=pyunits.dimensionless, + units=t_units, ) b.add_component("s" + suffix, s) @@ -124,15 +120,17 @@ def rule_teq(b): return ( b._teq[phase_pair] # pylint: disable=protected-access - b.temperature - - s[v_phase] * t_units - + s[l_phase] * t_units + - s[v_phase] + + s[l_phase] == 0 ) b.add_component("_teq_constraint" + suffix, Constraint(rule=rule_teq)) + # Epsilon variables will be given units of flow, as this is usually what dominates + # the complementarity equations. eps_t = Param( - default=1e-4, + default=1, mutable=True, doc="Smoothing parameter for temperature complementarity", units=f_units, @@ -140,7 +138,7 @@ def rule_teq(b): b.add_component("eps_t" + suffix, eps_t) eps_z = Param( - default=1e-4, + default=1, mutable=True, doc="Smoothing parameter for cubic root complementarities", units=f_units, @@ -168,7 +166,7 @@ def rule_teq(b): def rule_temperature_slack_complementarity(b, p): flow_phase = b.flow_mol_phase[p] - return smooth_min(s[p] * f_units, flow_phase, eps_t) == 0 + return smooth_min(s[p] * f_units / t_units, flow_phase, eps_t) == 0 b.add_component( "temperature_slack_complementarity" + suffix, diff --git a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py index ffb120db29..6454c44c1e 100644 --- a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py @@ -209,9 +209,9 @@ def test_build(frame): assert isinstance(frame.props[1].cubic_second_derivative_Vap_Liq, Expression) assert isinstance(frame.props[1].eps_t_Vap_Liq, Param) - assert value(frame.props[1].eps_t_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) + assert value(frame.props[1].eps_t_Vap_Liq) == pytest.approx(1, rel=1e-8) assert isinstance(frame.props[1].eps_z_Vap_Liq, Param) - assert value(frame.props[1].eps_z_Vap_Liq) == pytest.approx(1e-4, rel=1e-8) + assert value(frame.props[1].eps_z_Vap_Liq) == pytest.approx(1, rel=1e-8) assert isinstance(frame.props[1]._teq_constraint_Vap_Liq, Constraint) assert isinstance( diff --git a/idaes/models/unit_models/tests/test_heat_exchanger.py b/idaes/models/unit_models/tests/test_heat_exchanger.py index 81d28580a9..a7dc407e4b 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger.py @@ -1683,6 +1683,13 @@ def btx(self): m.fs.unit.cold_side.scaling_factor_pressure = 1 + # Set small values of epsilon to get sufficiently accurate results + # Only applies ot hot side, as cold side used the original SmoothVLE. + m.fs.unit.hot_side.properties_in[0].eps_t_Vap_Liq.set_value(1e-4) + m.fs.unit.hot_side.properties_in[0].eps_z_Vap_Liq.set_value(1e-4) + m.fs.unit.hot_side.properties_out[0].eps_t_Vap_Liq.set_value(1e-4) + m.fs.unit.hot_side.properties_out[0].eps_z_Vap_Liq.set_value(1e-4) + return m @pytest.mark.build @@ -2055,6 +2062,13 @@ def test_hx0d_initializer(self): model.fs.unit.cold_side.scaling_factor_pressure = 1 + # Set small values of epsilon to get sufficiently accurate results + # Only applies ot hot side, as cold side used the original SmoothVLE. + model.fs.unit.hot_side.properties_in[0].eps_t_Vap_Liq.set_value(1e-4) + model.fs.unit.hot_side.properties_in[0].eps_z_Vap_Liq.set_value(1e-4) + model.fs.unit.hot_side.properties_out[0].eps_t_Vap_Liq.set_value(1e-4) + model.fs.unit.hot_side.properties_out[0].eps_z_Vap_Liq.set_value(1e-4) + initializer = HX0DInitializer() initializer.initialize(model.fs.unit) diff --git a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py index 7a4cd24ef4..7cbd2d1753 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger_1D.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger_1D.py @@ -2448,6 +2448,12 @@ def btx(self): m.fs.unit.cold_side_inlet.mole_frac_comp[0, "benzene"].fix(0.5) m.fs.unit.cold_side_inlet.mole_frac_comp[0, "toluene"].fix(0.5) + # Set small values of epsilon to get sufficiently accurate results + # Only need hot side, as cold side uses old SmoothVLE + for i in m.fs.unit.hot_side.properties.keys(): + m.fs.unit.hot_side.properties[i].eps_t_Vap_Liq.set_value(1e-4) + m.fs.unit.hot_side.properties[i].eps_z_Vap_Liq.set_value(1e-4) + return m @pytest.mark.component @@ -3531,6 +3537,11 @@ def model(self): m.fs.unit.cold_side_inlet.mole_frac_comp[0, "benzene"].set_value(0.5) m.fs.unit.cold_side_inlet.mole_frac_comp[0, "toluene"].set_value(0.5) + # Set small values of epsilon to get sufficiently accurate results + for i in m.fs.unit.hot_side.properties.keys(): + m.fs.unit.hot_side.properties[i].eps_t_Vap_Liq.set_value(1e-4) + m.fs.unit.hot_side.properties[i].eps_z_Vap_Liq.set_value(1e-4) + return m @pytest.mark.component diff --git a/idaes/models/unit_models/tests/test_heater.py b/idaes/models/unit_models/tests/test_heater.py index 4657beb558..6f67b13f11 100644 --- a/idaes/models/unit_models/tests/test_heater.py +++ b/idaes/models/unit_models/tests/test_heater.py @@ -511,6 +511,12 @@ def btg(self): m.fs.unit.heat_duty.fix(-5000) m.fs.unit.deltaP.fix(0) + # Set small values of epsilon to get sufficiently accurate results + m.fs.unit.control_volume.properties_in[0].eps_t_Vap_Liq.set_value(1e-4) + m.fs.unit.control_volume.properties_in[0].eps_z_Vap_Liq.set_value(1e-4) + m.fs.unit.control_volume.properties_out[0].eps_t_Vap_Liq.set_value(1e-4) + m.fs.unit.control_volume.properties_out[0].eps_z_Vap_Liq.set_value(1e-4) + return m @pytest.mark.build @@ -667,6 +673,13 @@ def model(self): m.fs.unit.heat_duty.fix(-5000) m.fs.unit.deltaP.fix(0) + # Set small values of epsilon to get sufficiently accurate results + m.fs.unit.control_volume.properties_in.display() + m.fs.unit.control_volume.properties_in[0].eps_t_Vap_Liq.set_value(1e-4) + m.fs.unit.control_volume.properties_in[0].eps_z_Vap_Liq.set_value(1e-4) + m.fs.unit.control_volume.properties_out[0].eps_t_Vap_Liq.set_value(1e-4) + m.fs.unit.control_volume.properties_out[0].eps_z_Vap_Liq.set_value(1e-4) + return m @pytest.mark.integration diff --git a/idaes/models/unit_models/tests/test_shell_and_tube_1D.py b/idaes/models/unit_models/tests/test_shell_and_tube_1D.py index 0c7207f576..bdb087e3cb 100644 --- a/idaes/models/unit_models/tests/test_shell_and_tube_1D.py +++ b/idaes/models/unit_models/tests/test_shell_and_tube_1D.py @@ -1420,6 +1420,12 @@ def btx(self): m.fs.unit.cold_side_inlet.mole_frac_comp[0, "benzene"].fix(0.5) m.fs.unit.cold_side_inlet.mole_frac_comp[0, "toluene"].fix(0.5) + # Set small values of epsilon to get sufficiently accurate results + # Only need hot side, as cold side uses old SmoothVLE + for i in m.fs.unit.hot_side.properties.keys(): + m.fs.unit.hot_side.properties[i].eps_t_Vap_Liq.set_value(1e-4) + m.fs.unit.hot_side.properties[i].eps_z_Vap_Liq.set_value(1e-4) + return m @pytest.mark.component From a98597c9c017fdd5dc2116687a4bf3365dc0fa17 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Tue, 28 May 2024 13:12:35 -0400 Subject: [PATCH 30/33] Fixing typo --- idaes/models/unit_models/tests/test_heat_exchanger.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idaes/models/unit_models/tests/test_heat_exchanger.py b/idaes/models/unit_models/tests/test_heat_exchanger.py index a7dc407e4b..fdbc4d90e9 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger.py @@ -1684,7 +1684,7 @@ def btx(self): m.fs.unit.cold_side.scaling_factor_pressure = 1 # Set small values of epsilon to get sufficiently accurate results - # Only applies ot hot side, as cold side used the original SmoothVLE. + # Only applies to hot side, as cold side used the original SmoothVLE. m.fs.unit.hot_side.properties_in[0].eps_t_Vap_Liq.set_value(1e-4) m.fs.unit.hot_side.properties_in[0].eps_z_Vap_Liq.set_value(1e-4) m.fs.unit.hot_side.properties_out[0].eps_t_Vap_Liq.set_value(1e-4) @@ -2063,7 +2063,7 @@ def test_hx0d_initializer(self): model.fs.unit.cold_side.scaling_factor_pressure = 1 # Set small values of epsilon to get sufficiently accurate results - # Only applies ot hot side, as cold side used the original SmoothVLE. + # Only applies to hot side, as cold side used the original SmoothVLE. model.fs.unit.hot_side.properties_in[0].eps_t_Vap_Liq.set_value(1e-4) model.fs.unit.hot_side.properties_in[0].eps_z_Vap_Liq.set_value(1e-4) model.fs.unit.hot_side.properties_out[0].eps_t_Vap_Liq.set_value(1e-4) From 8b951b8aaa35b78e3187772dd1a2704e5dd43e9b Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 13 Jun 2024 15:36:40 -0400 Subject: [PATCH 31/33] Fixing pint version issue --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1d914cb421..47fbb64616 100644 --- a/setup.py +++ b/setup.py @@ -133,7 +133,7 @@ def __getitem__(self, key): # Concrete dependencies go in requirements[-dev].txt install_requires=[ "pyomo >= 6.7.3", - "pint", # required to use Pyomo units + "pint<0.24", # required to use Pyomo units "networkx", # required to use Pyomo network "numpy<2", # pandas constraint added on 2023-08-30 b/c bug in v2.1 From cbaf0aa448a75e87b246f1f6a4d8a9897955939a Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 17 Jun 2024 11:20:58 -0400 Subject: [PATCH 32/33] Addressing comments --- .../general/pe/smooth_flash.rst | 2 +- .../general/pe/smooth_vle2.rst | 10 ++++---- .../properties/modular_properties/eos/ceos.py | 24 +++++++++---------- .../modular_properties/examples/BT_PR.py | 6 +++-- .../phase_equil/__init__.py | 2 +- .../phase_equil/smooth_VLE_2.py | 10 ++++---- .../phase_equil/tests/test_smooth_VLE_2.py | 14 +++++------ .../unit_models/tests/test_heat_exchanger.py | 2 +- 8 files changed, 36 insertions(+), 34 deletions(-) diff --git a/docs/explanations/components/property_package/general/pe/smooth_flash.rst b/docs/explanations/components/property_package/general/pe/smooth_flash.rst index 95ea4a9d36..7ba6fd0697 100644 --- a/docs/explanations/components/property_package/general/pe/smooth_flash.rst +++ b/docs/explanations/components/property_package/general/pe/smooth_flash.rst @@ -5,7 +5,7 @@ Smooth Vapor-Liquid Equilibrium Formulation (SmoothVLE) :depth: 2 .. note:: - For property packages using cubic Equations of State, there is an alternative :ref:`SmoothVLE2 ` class that may give better performance. + For property packages using cubic Equations of State, there is an alternative :ref:`CubicComplementarityVLE ` class that may give better performance. Source ------ diff --git a/docs/explanations/components/property_package/general/pe/smooth_vle2.rst b/docs/explanations/components/property_package/general/pe/smooth_vle2.rst index 1516eada69..0afa557f5f 100644 --- a/docs/explanations/components/property_package/general/pe/smooth_vle2.rst +++ b/docs/explanations/components/property_package/general/pe/smooth_vle2.rst @@ -1,5 +1,5 @@ -Cubic Smooth Vapor-Liquid Equilibrium Formulation (SmoothVLE2) -============================================================== +Cubic Smooth Vapor-Liquid Equilibrium Formulation (CubicComplementarityVLE) +=========================================================================== .. contents:: Contents :depth: 2 @@ -15,9 +15,9 @@ Dabadghao, V., Ghouse, J., Eslick, J., Lee, A., Burgard, A., Miller, D., and Bie Introduction ------------ -Typically, equilibrium calculations are only used when the user knows the current state is within the two-phase envelope. For simulation only studies, the user may know a priori the condition of the stream but when the same set of equations are used for optimization, there is a high probability that the specifications can transcend the phase envelope. In these situations, the equilibrium calculations become trivial, thus it is necessary to find a formulation that has non-trivial solutions at all states. +Often, a user may not know whether a state corresponds to a liquid, gas, or coexisting mixture. Even if a user knows the phase composition of a problem's initial condition, optimization may push the stream into or out of the two-phase region. Therefore, it is necessary to formulate phase equilibrium equations that are well-behaved for both one-phase and two-phase streams. -To address this, the cubic smooth vapor-liquid equilibrium (VLE) formulation always solves the equilibrium calculations at a condition where a valid two-phase solution exists. In situations where only a single phase is present, the phase equilibrium is solved at the either the bubble or dew point, where the non-existent phase exists but in negligible amounts. In this way, a non-trivial solution is guaranteed but still gives near-zero material in the non-existent phase in the single phase regions. Rather than explicitly calculate the bubble and dew points (as is done in the non-cubic formulation), this formulation leverages properties of the cubic equation of state to identify the "equilibrium temperature". +To address this, the cubic smooth vapor-liquid equilibrium (VLE) formulation always solves the equilibrium equations at a condition where a valid two-phase solution exists. In situations where only a single phase is present, the phase equilibrium is solved at the either the bubble or dew point, where the second phase is just beginning to form; in this way, a non-trivial solution is guaranteed. Rather than explicitly calculate the bubble and dew points (as is done in the :ref:`non-cubic formulation `), this formulation leverages properties of the cubic equation of state to identify the "equilibrium temperature". Formulation ----------- @@ -39,7 +39,7 @@ In order to determine the values of :math:`s_{liq}` and :math:`s_{vap}`, the fol .. math:: 0 = \min(s_{liq}, F_{liq}) .. math:: 0 = \min(s_{vap}, F_{vap}) -where :math:`F_{p}` is the flow rate of each phase :math:`p`. That is, for each phase (liquid and vapor), if thee is any flowrate associated with that phase (i.e., the phase exists), its slack variable must be equal to zero. +where :math:`F_{p}` is the flow rate of each phase :math:`p`. That is, for each phase (liquid and vapor), if there is any flowrate associated with that phase (i.e., the phase exists), its slack variable must be equal to zero. Additionally, the follow complementarities are written to constraint the roots of the cubic equation of state. diff --git a/idaes/models/properties/modular_properties/eos/ceos.py b/idaes/models/properties/modular_properties/eos/ceos.py index 7128ba2021..4288fdc680 100644 --- a/idaes/models/properties/modular_properties/eos/ceos.py +++ b/idaes/models/properties/modular_properties/eos/ceos.py @@ -665,7 +665,7 @@ def dens_mol_phase(b, p): @staticmethod def energy_internal_mol_phase(blk, p): """ - Phase specific internal energy + Phase molar internal energy """ pobj = blk.params.get_phase(p) @@ -697,7 +697,7 @@ def energy_internal_mol_phase(blk, p): @staticmethod def energy_internal_mol_phase_comp(blk, p, j): """ - Phase partial specific internal energy + Phase partial molar internal energy """ return ( blk.enth_mol_phase_comp[p, j] - blk.pressure * blk.vol_mol_phase_comp[p, j] @@ -706,7 +706,7 @@ def energy_internal_mol_phase_comp(blk, p, j): @staticmethod def enth_mol_phase(blk, p): """ - Phase specific enthalpy + Phase molar enthalpy """ pobj = blk.params.get_phase(p) @@ -753,7 +753,7 @@ def enth_mol_phase_comp(blk, p, j): @staticmethod def entr_mol_phase(blk, p): """ - Phase specific entropy + Phase molar entropy """ pobj = blk.params.get_phase(p) @@ -1423,7 +1423,7 @@ def calculate_equilibrium_cubic_coefficients(b, cubic_name, cubic_type, p1, p2, def func_fw_PR(cobj): """ - fw function for Peng-Robinson EoS. + f(omega) function for Peng-Robinson EoS. Args: cobj: Component object @@ -1437,7 +1437,7 @@ def func_fw_PR(cobj): def func_fw_SRK(cobj): """ - fw function for SRK EoS. + f(omega) function for SRK EoS. Args: cobj: Component object @@ -1468,7 +1468,7 @@ def func_alpha_soave(T, fw, cobj): def func_dalpha_dT_soave(T, fw, cobj): """ - Function to get first derivative of Soave alpha function. + Function to get first partial derivative w.r.t. temperature of Soave alpha function. Args: fw: expression for fw @@ -1485,7 +1485,7 @@ def func_dalpha_dT_soave(T, fw, cobj): def func_d2alpha_dT2_soave(T, fw, cobj): """ - Function to get 2nd derivative of Soave alpha function. + Function to get 2nd partial derivative w.r.t. temperature of Soave alpha function. Args: fw: expression for fw @@ -1504,7 +1504,7 @@ def func_d2alpha_dT2_soave(T, fw, cobj): # Mixing rules def rule_am_default(m, cname, a, p, pp=()): """ - Standard mixing rule for a term + Standard Van der Waals one-fluid mixing rule for a term """ k = getattr(m.params, cname + "_kappa") return sum( @@ -1521,7 +1521,7 @@ def rule_am_default(m, cname, a, p, pp=()): def rule_am_crit_default(m, cname, a_crit): """ - Standard mixing rule for a term evaluated at critical point + Standard Van der Waals one-fluid mixing rule for a term evaluated at critical point """ k = getattr(m.params, cname + "_kappa") return sum( @@ -1538,13 +1538,13 @@ def rule_am_crit_default(m, cname, a_crit): def rule_bm_default(m, b, p): """ - Standard mixing rule for b term + Standard Van der Waals one-fluid mixing rule for b term """ return sum(m.mole_frac_phase_comp[p, i] * b[i] for i in m.components_in_phase(p)) def rule_bm_crit_default(m, b): """ - Standard mixing rule for b term evaluated at critical point + Standard Van der Waals one-fluid mixing rule for b term evaluated at critical point """ return sum(m.mole_frac_comp[i] * b[i] for i in m.component_list) diff --git a/idaes/models/properties/modular_properties/examples/BT_PR.py b/idaes/models/properties/modular_properties/examples/BT_PR.py index 1c0710752e..7341a3992e 100644 --- a/idaes/models/properties/modular_properties/examples/BT_PR.py +++ b/idaes/models/properties/modular_properties/examples/BT_PR.py @@ -31,7 +31,9 @@ from idaes.models.properties.modular_properties.state_definitions import FTPx from idaes.models.properties.modular_properties.eos.ceos import Cubic, CubicType -from idaes.models.properties.modular_properties.phase_equil import SmoothVLE2 +from idaes.models.properties.modular_properties.phase_equil import ( + CubicComplementarityVLE, +) from idaes.models.properties.modular_properties.phase_equil.bubble_dew import ( LogBubbleDew, ) @@ -148,7 +150,7 @@ "temperature_ref": (298.15, pyunits.K), # Defining phase equilibria "phases_in_equilibrium": [("Vap", "Liq")], - "phase_equilibrium_state": {("Vap", "Liq"): SmoothVLE2}, + "phase_equilibrium_state": {("Vap", "Liq"): CubicComplementarityVLE}, "bubble_dew_method": LogBubbleDew, "parameter_data": { "PR_kappa": { diff --git a/idaes/models/properties/modular_properties/phase_equil/__init__.py b/idaes/models/properties/modular_properties/phase_equil/__init__.py index 53e32aba81..b9a28fde2f 100644 --- a/idaes/models/properties/modular_properties/phase_equil/__init__.py +++ b/idaes/models/properties/modular_properties/phase_equil/__init__.py @@ -11,4 +11,4 @@ # for full copyright and license information. ################################################################################# from .smooth_VLE import SmoothVLE -from .smooth_VLE_2 import SmoothVLE2 +from .smooth_VLE_2 import CubicComplementarityVLE diff --git a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py index 86912741df..2f2bd30263 100644 --- a/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/smooth_VLE_2.py @@ -48,7 +48,7 @@ # ----------------------------------------------------------------------------- -class SmoothVLE2: +class CubicComplementarityVLE: """ Improved Vapor-Liquid Equilibrium complementarity formulation for Cubic Equations of State. """ @@ -75,7 +75,7 @@ def phase_equil(b, phase_pair): if l_phase is None or v_phase is None: raise ConfigurationError( f"{b.params.name} - Generic Property Package phase pair {phase_pair[0]}-{phase_pair[1]} " - "was set to use Smooth VLE formulation, however this is not a vapor-liquid pair." + "was set to use CubicComplementarityVLE formulation, however this is not a vapor-liquid pair." ) try: @@ -90,12 +90,12 @@ def phase_equil(b, phase_pair): != vobj.config.equation_of_state_options ): raise ConfigurationError( - f"{b.params.name} - SmoothVLE2 formulation requires that both phases use the same " - "type of cubic equation of state." + f"{b.params.name} - CubicComplementarityVLE formulation requires that both " + "phases use the same type of cubic equation of state." ) except AttributeError: raise ConfigurationError( - f"{b.params.name} - SmoothVLE2 formulation only supports cubic equations of state." + f"{b.params.name} - CubicComplementarityVLE formulation only supports cubic equations of state." ) # Definition of equilibrium temperature for smooth VLE diff --git a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py index 6454c44c1e..5884aa3d6e 100644 --- a/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py +++ b/idaes/models/properties/modular_properties/phase_equil/tests/test_smooth_VLE_2.py @@ -11,7 +11,7 @@ # for full copyright and license information. ################################################################################# """ -Tests for SmoothVLE2 formulation +Tests for CubicComplementarityVLE formulation Authors: Andrew Lee """ @@ -33,7 +33,7 @@ ) from idaes.models.properties.modular_properties.state_definitions import FTPx from idaes.models.properties.modular_properties.phase_equil.smooth_VLE_2 import ( - SmoothVLE2, + CubicComplementarityVLE, _calculate_temperature_slacks, _calculate_ceos_derivative_slacks, EPS_INIT, @@ -80,7 +80,7 @@ def test_different_cubics(): "temperature": pyunits.K, }, phases_in_equilibrium=[("Vap", "Liq")], - phase_equilibrium_state={("Vap", "Liq"): SmoothVLE2}, + phase_equilibrium_state={("Vap", "Liq"): CubicComplementarityVLE}, parameter_data={ "PR_kappa": { ("H2O", "H2O"): 0.000, @@ -93,7 +93,7 @@ def test_different_cubics(): with pytest.raises( ConfigurationError, - match="params - SmoothVLE2 formulation requires that both phases use the same " + match="params - CubicComplementarityVLE formulation requires that both phases use the same " "type of cubic equation of state.", ): m.props = m.params.state_block_class([1], parameters=m.params) @@ -134,7 +134,7 @@ def test_non_cubic(): "temperature": pyunits.K, }, phases_in_equilibrium=[("Vap", "Liq")], - phase_equilibrium_state={("Vap", "Liq"): SmoothVLE2}, + phase_equilibrium_state={("Vap", "Liq"): CubicComplementarityVLE}, parameter_data={ "PR_kappa": { ("H2O", "H2O"): 0.000, @@ -144,7 +144,7 @@ def test_non_cubic(): with pytest.raises( ConfigurationError, - match="params - SmoothVLE2 formulation only supports cubic equations of state.", + match="params - CubicComplementarityVLE formulation only supports cubic equations of state.", ): m.props = m.params.state_block_class([1], parameters=m.params) @@ -186,7 +186,7 @@ def frame(): "temperature": pyunits.K, }, phases_in_equilibrium=[("Vap", "Liq")], - phase_equilibrium_state={("Vap", "Liq"): SmoothVLE2}, + phase_equilibrium_state={("Vap", "Liq"): CubicComplementarityVLE}, parameter_data={ "PR_kappa": { ("H2O", "H2O"): 0.000, diff --git a/idaes/models/unit_models/tests/test_heat_exchanger.py b/idaes/models/unit_models/tests/test_heat_exchanger.py index fdbc4d90e9..ce671036cd 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger.py @@ -2098,7 +2098,7 @@ def test_hx0d_initializer(self): def test_block_triangularization( self, ): - # Trying to get this to work with SmoothVLE2 is challenging, and + # Trying to get this to work with CubicComplementarityVLE is challenging, and # not necessary for this particular test new_config = deepcopy(configuration) new_config["phase_equilibrium_state"] = {("Vap", "Liq"): SmoothVLE} From c2a593d447cc3bfbce18c3f47a205cea7200672e Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Mon, 17 Jun 2024 12:35:47 -0400 Subject: [PATCH 33/33] Fixing broken link in docs --- .../components/property_package/general/pe/smooth_flash.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/explanations/components/property_package/general/pe/smooth_flash.rst b/docs/explanations/components/property_package/general/pe/smooth_flash.rst index 7ba6fd0697..2264da6552 100644 --- a/docs/explanations/components/property_package/general/pe/smooth_flash.rst +++ b/docs/explanations/components/property_package/general/pe/smooth_flash.rst @@ -5,7 +5,7 @@ Smooth Vapor-Liquid Equilibrium Formulation (SmoothVLE) :depth: 2 .. note:: - For property packages using cubic Equations of State, there is an alternative :ref:`CubicComplementarityVLE ` class that may give better performance. + For property packages using cubic Equations of State, there is an alternative :ref:`CubicComplementarityVLE ` class that may give better performance. Source ------