From 53e9feb158c766b7070759f5edc7f00328ada3b3 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 31 Aug 2023 23:48:31 -0400 Subject: [PATCH] Fixing handling of useDefault in lumped capacitance HX model (#1259) * Fixing usage of config.property_package * Fixing issues in HX_LC model * Revert "Fixing issues in HX_LC model" This reverts commit 5a751db8791253c8edcff55b3d295f1cccd852ec. * Fixing issues with HX_LC * Fixing typo --------- Co-authored-by: Ludovico Bianchi --- idaes/models/unit_models/heat_exchanger_lc.py | 63 +++++++++-- .../tests/test_heat_exchanger_lc.py | 101 ++++++++++++++++-- 2 files changed, 149 insertions(+), 15 deletions(-) diff --git a/idaes/models/unit_models/heat_exchanger_lc.py b/idaes/models/unit_models/heat_exchanger_lc.py index 8c75bf7de4..53fd069c47 100644 --- a/idaes/models/unit_models/heat_exchanger_lc.py +++ b/idaes/models/unit_models/heat_exchanger_lc.py @@ -21,7 +21,7 @@ from pyomo.common.config import ConfigValue from idaes.core import declare_process_block_class, useDefault from idaes.core.util.config import DefaultBool -from idaes.core.util.exceptions import ConfigurationError, IdaesError +from idaes.core.util.exceptions import ConfigurationError, IdaesError, DynamicError from .heat_exchanger import HeatExchangerData @@ -113,6 +113,60 @@ def _add_wall_variables(self): units=ua_units, ) + def _setup_dynamics(self): + """ + The Lumped Capacitance Heat Exchanger model is different from other + unit models in that it allows for part of the heat balance to be dynamic + whilst the control volumes are steady state. Thus, we need a custom + _setup_dynamics method. + + Args: + None + + Returns: + None + """ + # Get flowsheet dynamic flag + dynamic = self.flowsheet().config.dynamic + + # First, check the dynamic_heat_balance argument + if self.config.dynamic_heat_balance is useDefault: + # If use default, use flowsheet flag + self.config.dynamic_heat_balance = dynamic + elif self.config.dynamic_heat_balance and not dynamic: + # If True, flowsheet must also be dynamic + raise DynamicError( + "{} trying to declare a dynamic model within " + "a steady-state flowsheet. This is not " + "supported by the IDAES framework. Try " + "creating a dynamic flowsheet instead, and " + "declaring some models as steady-state.".format(self.name) + ) + + # Next, check the dynamic flag + if self.config.dynamic == useDefault: + # Use same setting as dynamic_heat_balance + self.config.dynamic = self.config.dynamic_heat_balance + elif self.config.dynamic and not self.config.dynamic_heat_balance: + # If True, must also have dynamic_heat_balance = True + raise ConfigurationError( + "{} dynamic can only be True if dynamic_heat_balance " + "is also True.".format(self.name) + ) + + # Set and validate has_holdup argument + if self.config.has_holdup == useDefault: + # Default to same value as dynamic flag + self.config.has_holdup = self.config.dynamic + elif self.config.has_holdup is False: + if self.config.dynamic is True: + # Dynamic model must have has_holdup = True + raise ConfigurationError( + "{} invalid arguments for dynamic and has_holdup. " + "If dynamic = True, has_holdup must also be True " + "(was False)".format(self.name) + ) + def _add_wall_variable_constraints(self): @self.Constraint( self.flowsheet().config.time, @@ -206,13 +260,6 @@ def build(self): temp_units = s1_metadata.get_derived_units("temperature") time_units = s1_metadata.get_derived_units("time") - if not self.flowsheet().config.dynamic: - raise ConfigurationError( - "{} dynamic heat balance cannot be " - "used with a steady-state " - "flowsheet".format(self.name) - ) - self.dT_wall_dt = DerivativeVar( self.temperature_wall, wrt=self.flowsheet().config.time, diff --git a/idaes/models/unit_models/tests/test_heat_exchanger_lc.py b/idaes/models/unit_models/tests/test_heat_exchanger_lc.py index 044a0e90ac..a6c42b91b3 100644 --- a/idaes/models/unit_models/tests/test_heat_exchanger_lc.py +++ b/idaes/models/unit_models/tests/test_heat_exchanger_lc.py @@ -35,7 +35,7 @@ from idaes.core.util.model_statistics import degrees_of_freedom from idaes.core.solvers import get_solver -from idaes.core.util.exceptions import ConfigurationError, IdaesError +from idaes.core.util.exceptions import DynamicError, ConfigurationError, IdaesError from idaes.core.util.testing import PhysicalParameterTestBlock, initialization_tester @@ -59,7 +59,6 @@ from idaes.models.unit_models.heat_exchanger import delta_temperature_lmtd_callback from idaes.models.properties.general_helmholtz import helmholtz_available from idaes.core.initialization import ( - BlockTriangularizationInitializer, InitializationStatus, ) @@ -368,12 +367,19 @@ def test_solve(self, model): assert check_optimal_termination(results) @pytest.mark.unit - def test_static_flowsheet(self, static_flowsheet_model): - m = static_flowsheet_model + def test_dynamic_heat_in_static_flowsheet(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = PhysicalParameterTestBlock() + with pytest.raises( - ConfigurationError, - match="dynamic heat balance cannot be used with a " - "steady-state flowsheet", + DynamicError, + match="trying to declare a dynamic model within " + "a steady-state flowsheet. This is not " + "supported by the IDAES framework. Try " + "creating a dynamic flowsheet instead, and " + "declaring some models as steady-state.", ): m.fs.unit = HeatExchangerLumpedCapacitance( hot_side_name="shell", @@ -384,6 +390,87 @@ def test_static_flowsheet(self, static_flowsheet_model): dynamic_heat_balance=True, ) + @pytest.mark.unit + def test_dynamic_cv_wo_dynamic_heat(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = PhysicalParameterTestBlock() + + with pytest.raises( + ConfigurationError, + match="dynamic can only be True if dynamic_heat_balance " "is also True.", + ): + m.fs.unit = HeatExchangerLumpedCapacitance( + hot_side_name="shell", + cold_side_name="tube", + shell={"property_package": m.fs.properties}, + tube={"property_package": m.fs.properties}, + dynamic=True, + dynamic_heat_balance=False, + ) + + @pytest.mark.unit + def test_default_in_dynamic_flowsheet(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=True, time_set=[0, 1], time_units=pyunits.s) + + m.fs.properties = PhysicalParameterTestBlock() + + m.fs.unit = HeatExchangerLumpedCapacitance( + hot_side_name="shell", + cold_side_name="tube", + shell={"property_package": m.fs.properties}, + tube={"property_package": m.fs.properties}, + ) + + assert m.fs.unit.config.dynamic_heat_balance + assert m.fs.unit.config.dynamic + assert m.fs.unit.config.has_holdup + + @pytest.mark.unit + def test_steady_state_cv_in_dynamic_flowsheet(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=True, time_set=[0, 1], time_units=pyunits.s) + + m.fs.properties = PhysicalParameterTestBlock() + + m.fs.unit = HeatExchangerLumpedCapacitance( + hot_side_name="shell", + cold_side_name="tube", + shell={"property_package": m.fs.properties}, + tube={"property_package": m.fs.properties}, + dynamic=False, + dynamic_heat_balance=True, + ) + + assert m.fs.unit.config.dynamic_heat_balance + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + + @pytest.mark.unit + def test_dynamic_wo_holdup(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=True, time_set=[0, 1], time_units=pyunits.s) + + m.fs.properties = PhysicalParameterTestBlock() + + with pytest.raises( + ConfigurationError, + match="invalid arguments for dynamic and has_holdup. " + "If dynamic = True, has_holdup must also be True " + "\(was False\)", + ): + m.fs.unit = HeatExchangerLumpedCapacitance( + hot_side_name="shell", + cold_side_name="tube", + shell={"property_package": m.fs.properties}, + tube={"property_package": m.fs.properties}, + dynamic=True, + dynamic_heat_balance=True, + has_holdup=False, + ) + @pytest.mark.unit def test_static_heat_balance(self, static_flowsheet_model): m = static_flowsheet_model