From 1a58c7c2f880986fecee1775e8755fd05a0ef9e5 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Fri, 3 Jan 2025 09:24:20 -0500 Subject: [PATCH 01/14] Extended control volumes --- idaes/core/__init__.py | 2 + idaes/core/base/control_volume0d.py | 10 + idaes/core/base/control_volume1d.py | 10 + idaes/core/base/control_volume_base.py | 15 + idaes/core/base/extended_control_volume0d.py | 109 + idaes/core/base/extended_control_volume1d.py | 118 + .../core/base/tests/test_control_volume_0d.py | 18 + .../core/base/tests/test_control_volume_1d.py | 24 + .../base/tests/test_control_volume_base.py | 2 +- .../tests/test_extended_control_volume_0d.py | 2703 +++++++++++++++++ 10 files changed, 3010 insertions(+), 1 deletion(-) create mode 100644 idaes/core/base/extended_control_volume0d.py create mode 100644 idaes/core/base/extended_control_volume1d.py create mode 100644 idaes/core/base/tests/test_extended_control_volume_0d.py diff --git a/idaes/core/__init__.py b/idaes/core/__init__.py index 59b7b036c0..7a36b78f7f 100644 --- a/idaes/core/__init__.py +++ b/idaes/core/__init__.py @@ -33,6 +33,8 @@ ) from .base.control_volume0d import ControlVolume0DBlock from .base.control_volume1d import ControlVolume1DBlock, DistributedVars +from .base.extended_control_volume0d import ExtendedControlVolume0DBlock +from .base.extended_control_volume1d import ExtendedControlVolume1DBlock from .base.phases import ( Phase, LiquidPhase, diff --git a/idaes/core/base/control_volume0d.py b/idaes/core/base/control_volume0d.py index 5eb8afa2e1..c177d504ca 100644 --- a/idaes/core/base/control_volume0d.py +++ b/idaes/core/base/control_volume0d.py @@ -1352,6 +1352,16 @@ def add_total_energy_balances(self, *args, **kwargs): "add_total_energy_balances.".format(self.name) ) + def add_isothermal_constraint(self, *args, **kwargs): + """ + Requires ExtendedControlVolume0D + """ + raise BalanceTypeNotSupportedError( + f"{self.name} ControlVolume0D does not support isothermal energy balances. " + "Please consider using ExtendedControlVolume0D in your model if you require " + "support for isothermal balances." + ) + def add_total_pressure_balances(self, has_pressure_change=False, custom_term=None): """ This method constructs a set of 0D pressure balances indexed by time. diff --git a/idaes/core/base/control_volume1d.py b/idaes/core/base/control_volume1d.py index ab98d373bb..03889f2d34 100644 --- a/idaes/core/base/control_volume1d.py +++ b/idaes/core/base/control_volume1d.py @@ -1737,6 +1737,16 @@ def add_total_energy_balances(self, *args, **kwargs): "add_total_energy_balances.".format(self.name) ) + def add_isothermal_constraint(self, *args, **kwargs): + """ + Requires ExtendedControlVolume1D + """ + raise BalanceTypeNotSupportedError( + f"{self.name} ControlVolume1D does not support isothermal energy balances. " + "Please consider using ExtendedControlVolume1D in your model if you require " + "support for isothermal balances." + ) + def add_total_pressure_balances(self, has_pressure_change=False, custom_term=None): """ This method constructs a set of 1D pressure balances indexed by time. diff --git a/idaes/core/base/control_volume_base.py b/idaes/core/base/control_volume_base.py index 0b50fb6639..b8dc520b73 100644 --- a/idaes/core/base/control_volume_base.py +++ b/idaes/core/base/control_volume_base.py @@ -71,6 +71,7 @@ class EnergyBalanceType(Enum): enthalpyTotal = 2 energyPhase = 3 energyTotal = 4 + isothermal = 5 # Enumerate options for momentum balances @@ -618,6 +619,8 @@ def add_energy_balances(self, balance_type=EnergyBalanceType.useDefault, **kwarg eb = self.add_total_energy_balances(**kwargs) elif balance_type == EnergyBalanceType.energyPhase: eb = self.add_phase_energy_balances(**kwargs) + elif balance_type == EnergyBalanceType.isothermal: + eb = self.add_isothermal_constraint(**kwargs) else: raise ConfigurationError( "{} invalid balance_type for add_energy_balances." @@ -843,6 +846,18 @@ def add_total_energy_balances(self, *args, **kwargs): "developer of the ControlVolume class you are using.".format(self.name) ) + def add_isothermal_constraint(self, *args, **kwargs): + """ + Method for adding an isothermal constraint to the control volume. + + See specific control volume documentation for details. + """ + raise NotImplementedError( + f"{self.name} control volume class has not implemented a method for " + "add_isothermal_constraint. Please contact the " + "developer of the ControlVolume class you are using." + ) + def add_phase_pressure_balances(self, *args, **kwargs): """ Method for adding pressure balances indexed by diff --git a/idaes/core/base/extended_control_volume0d.py b/idaes/core/base/extended_control_volume0d.py new file mode 100644 index 0000000000..9597917e0b --- /dev/null +++ b/idaes/core/base/extended_control_volume0d.py @@ -0,0 +1,109 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2024 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# +""" +0D Control Volume class with support for isothermal energy balance. +""" + +__author__ = "Andrew Lee" + +# Import Pyomo libraries +from pyomo.environ import Constraint, Reals, units as pyunits, Var, value + +# Import IDAES cores +from idaes.core.base.control_volume0d import ControlVolume0DBlockData +from idaes.core import declare_process_block_class +from idaes.core.util.exceptions import ConfigurationError + +import idaes.logger as idaeslog + +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class( + "ExtendedControlVolume0DBlock", + doc=""" + ExtendedControlVolume0DBlock is an extension of the ControlVolume0D + block with support for isothermal conditions in place of a formal + energy balance.""", +) +class ExtendedControlVolume0DBlockData(ControlVolume0DBlockData): + """ + Extended 0-Dimensional (Non-Discretized) ControlVolume Class + + This class extends the existing ControlVolume0DBlockData class + with support for isothermal energy balances. + """ + + def add_isothermal_constraint( + self, + has_heat_of_reaction=False, + has_heat_transfer=False, + has_work_transfer=False, + has_enthalpy_transfer=False, + custom_term=None, + ): + """ + This method constructs an isothermal constraint for the control volume. + + Arguments are supported for compatibility with other forms but must be False + or None otherwise an Exception is raised. + + Args: + has_heat_of_reaction: whether terms for heat of reaction should + be included in enthalpy balance + has_heat_transfer: whether terms for heat transfer should be + included in enthalpy balances + has_work_transfer: whether terms for work transfer should be + included in enthalpy balances + has_enthalpy_transfer: whether terms for enthalpy transfer due to + mass transfer should be included in enthalpy balance. This + should generally be the same as the has_mass_transfer + argument in the material balance methods + custom_term: a Python method which returns Pyomo expressions representing + custom terms to be included in enthalpy balances. + Method should accept time and phase list as arguments. + + Returns: + Constraint object representing enthalpy balances + """ + if has_heat_transfer: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option requires that has_heat_transfer is False. " + "If you are trying to solve for heat duty to achieve isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ) + if has_work_transfer: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option requires that has_work_transfer is False. " + "If you are trying to solve for work under isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ) + if has_enthalpy_transfer: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option does not support enthalpy transfer. " + ) + if has_heat_of_reaction: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option requires that has_heat_of_reaction is False. " + "If you are trying to solve for heat duty to achieve isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ) + if custom_term is not None: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option does not support custom terms. " + ) + + # Add isothermal constraint + @self.Constraint(self.flowsheet().time, doc="Energy balances") + def enthalpy_balances(b, t): + return b.properties_in[t].temperature == b.properties_out[t].temperature diff --git a/idaes/core/base/extended_control_volume1d.py b/idaes/core/base/extended_control_volume1d.py new file mode 100644 index 0000000000..3d35cd952c --- /dev/null +++ b/idaes/core/base/extended_control_volume1d.py @@ -0,0 +1,118 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2024 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# +""" +1D Control Volume class with support for isothermal energy balance. +""" + +__author__ = "Andrew Lee" + +# Import Pyomo libraries +from pyomo.environ import Constraint, Reals, units as pyunits, Var, value + +# Import IDAES cores +from idaes.core.base.control_volume1d import ControlVolume1DBlockData +from idaes.core import declare_process_block_class +from idaes.core.util.exceptions import ConfigurationError + +import idaes.logger as idaeslog + +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class( + "ExtendedControlVolume1DBlock", + doc=""" + ExtendedControlVolume1DBlock is an extension of the ControlVolume1D + block with support for isothermal conditions in place of a formal + energy balance.""", +) +class ExtendedControlVolume1DBlockData(ControlVolume1DBlockData): + """ + Extended 1-Dimensional ControlVolume Class + + This class extends the existing ControlVolume1DBlockData class + with support for isothermal energy balances. + """ + + def add_isothermal_constraint( + self, + has_heat_of_reaction=False, + has_heat_transfer=False, + has_work_transfer=False, + has_enthalpy_transfer=False, + custom_term=None, + ): + """ + This method constructs an isothermal constraint for the control volume. + + Arguments are supported for compatibility with other forms but must be False + or None otherwise an Exception is raised. + + Args: + has_heat_of_reaction: whether terms for heat of reaction should + be included in enthalpy balance + has_heat_transfer: whether terms for heat transfer should be + included in enthalpy balances + has_work_transfer: whether terms for work transfer should be + included in enthalpy balances + has_enthalpy_transfer: whether terms for enthalpy transfer due to + mass transfer should be included in enthalpy balance. This + should generally be the same as the has_mass_transfer + argument in the material balance methods + custom_term: a Python method which returns Pyomo expressions representing + custom terms to be included in enthalpy balances. + Method should accept time and phase list as arguments. + + Returns: + Constraint object representing enthalpy balances + """ + if has_heat_transfer: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option requires that has_heat_transfer is False. " + "If you are trying to solve for heat duty to achieve isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ) + if has_work_transfer: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option requires that has_work_transfer is False. " + "If you are trying to solve for work under isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ) + if has_enthalpy_transfer: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option does not support enthalpy transfer. " + ) + if has_heat_of_reaction: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option requires that has_heat_of_reaction is False. " + "If you are trying to solve for heat duty to achieve isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ) + if custom_term is not None: + raise ConfigurationError( + f"{self.name}: isothermal energy balance option does not support custom terms. " + ) + + # Add isothermal constraint + @self.Constraint(self.flowsheet().time, doc="Energy balances") + def enthalpy_balances(b, t, x): + if ( + b.config.transformation_scheme != "FORWARD" + and x == b.length_domain.first() + ) or ( + b.config.transformation_scheme == "FORWARD" + and x == b.length_domain.last() + ): + return Constraint.Skip + else: + return b.properties[t, b.length_domain.prev(x)].temperature == b.properties[t, x].temperature diff --git a/idaes/core/base/tests/test_control_volume_0d.py b/idaes/core/base/tests/test_control_volume_0d.py index 42b8c1b7cc..30df1674df 100644 --- a/idaes/core/base/tests/test_control_volume_0d.py +++ b/idaes/core/base/tests/test_control_volume_0d.py @@ -2280,6 +2280,24 @@ def test_add_total_energy_balances(): m.fs.cv.add_total_energy_balances() +@pytest.mark.unit +def test_add_isothermal_energy_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_isothermal_constraint() + + # ----------------------------------------------------------------------------- # Test add total pressure balances @pytest.mark.unit diff --git a/idaes/core/base/tests/test_control_volume_1d.py b/idaes/core/base/tests/test_control_volume_1d.py index b92d56c300..3ede162772 100644 --- a/idaes/core/base/tests/test_control_volume_1d.py +++ b/idaes/core/base/tests/test_control_volume_1d.py @@ -3442,6 +3442,30 @@ def test_add_total_energy_balances(): m.fs.cv.add_total_energy_balances() +@pytest.mark.unit +def test_add_isothermal_energy_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume1DBlock( + property_package=m.fs.pp, + reaction_package=m.fs.rp, + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + finite_elements=10, + ) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_isothermal_constraint() + + # ----------------------------------------------------------------------------- # Test add total pressure balances @pytest.mark.unit diff --git a/idaes/core/base/tests/test_control_volume_base.py b/idaes/core/base/tests/test_control_volume_base.py index 7c282db1a1..9bd2b53e6e 100644 --- a/idaes/core/base/tests/test_control_volume_base.py +++ b/idaes/core/base/tests/test_control_volume_base.py @@ -55,7 +55,7 @@ def test_material_balance_type(): @pytest.mark.unit def test_energy_balance_type(): - assert len(EnergyBalanceType) == 6 + assert len(EnergyBalanceType) == 7 # Test that error is raised when given non-member with pytest.raises(AttributeError): diff --git a/idaes/core/base/tests/test_extended_control_volume_0d.py b/idaes/core/base/tests/test_extended_control_volume_0d.py new file mode 100644 index 0000000000..516f1dc2fa --- /dev/null +++ b/idaes/core/base/tests/test_extended_control_volume_0d.py @@ -0,0 +1,2703 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2024 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# +""" +Tests for ExtendedControlVolumeBlockData. + +Author: Andrew Lee +""" +import pytest +from pyomo.environ import ConcreteModel, Constraint, Expression, Set, units, Var +from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent +from pyomo.common.config import ConfigBlock +from idaes.core import ( + ExtendedControlVolume0DBlock as ControlVolume0DBlock, + ControlVolumeBlockData, + FlowsheetBlockData, + declare_process_block_class, + FlowDirection, + MaterialBalanceType, + EnergyBalanceType, +) +from idaes.core.util.exceptions import ( + BalanceTypeNotSupportedError, + ConfigurationError, + PropertyNotSupportedError, +) +from idaes.core.util.testing import ( + PhysicalParameterTestBlock, + ReactionParameterTestBlock, +) +import idaes.logger as idaeslog + + +# ----------------------------------------------------------------------------- +# Mockup classes for testing +@declare_process_block_class("Flowsheet") +class _Flowsheet(FlowsheetBlockData): + def build(self): + super(_Flowsheet, self).build() + + +@declare_process_block_class("CVFrame") +class CVFrameData(ControlVolume0DBlock): + def build(self): + super(ControlVolumeBlockData, self).build() + + +# ----------------------------------------------------------------------------- +# Basic tests +@pytest.mark.unit +def test_base_build(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + assert len(m.fs.cv.config) == 7 + assert m.fs.cv.config.dynamic is False + assert m.fs.cv.config.has_holdup is False + assert m.fs.cv.config.property_package == m.fs.pp + assert isinstance(m.fs.cv.config.property_package_args, ConfigBlock) + assert len(m.fs.cv.config.property_package_args) == 0 + assert m.fs.cv.config.reaction_package is None + assert isinstance(m.fs.cv.config.reaction_package_args, ConfigBlock) + assert len(m.fs.cv.config.reaction_package_args) == 0 + assert m.fs.cv.config.auto_construct is False + + assert hasattr(m.fs.config, "time") + + +# ----------------------------------------------------------------------------- +# Test add_geometry +@pytest.mark.unit +def test_add_geometry(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_geometry() + + assert hasattr(m.fs.cv, "volume") + assert len(m.fs.cv.volume) == 1.0 + assert m.fs.cv.volume[0].value == 1.0 + + +# ----------------------------------------------------------------------------- +# Test add_state_blocks +@pytest.mark.unit +def test_add_state_blocks(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + assert hasattr(m.fs.cv, "properties_in") + assert len(m.fs.cv.properties_in[0].config) == 3 + assert m.fs.cv.properties_in[0].config.defined_state is True + assert m.fs.cv.properties_in[0].config.has_phase_equilibrium is False + assert m.fs.cv.properties_in[0].config.parameters == m.fs.pp + + assert hasattr(m.fs.cv, "properties_out") + assert len(m.fs.cv.properties_out[0].config) == 3 + assert m.fs.cv.properties_out[0].config.defined_state is False + assert m.fs.cv.properties_out[0].config.has_phase_equilibrium is False + assert m.fs.cv.properties_out[0].config.parameters == m.fs.pp + + +@pytest.mark.unit +def test_add_state_block_forward_flow(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks( + information_flow=FlowDirection.forward, has_phase_equilibrium=False + ) + + assert m.fs.cv.properties_in[0].config.defined_state is True + assert m.fs.cv.properties_out[0].config.defined_state is False + + +@pytest.mark.unit +def test_add_state_block_backward_flow(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks( + information_flow=FlowDirection.backward, has_phase_equilibrium=False + ) + + assert m.fs.cv.properties_in[0].config.defined_state is False + assert m.fs.cv.properties_out[0].config.defined_state is True + + +@pytest.mark.unit +def test_add_state_blocks_has_phase_equilibrium(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + + assert m.fs.cv.properties_in[0].config.has_phase_equilibrium is True + assert m.fs.cv.properties_out[0].config.has_phase_equilibrium is True + + +@pytest.mark.unit +def test_add_state_blocks_no_has_phase_equilibrium(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_state_blocks() + + +@pytest.mark.unit +def test_add_state_blocks_custom_args(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, property_package_args={"test": "test"} + ) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + assert len(m.fs.cv.properties_in[0].config) == 4 + assert m.fs.cv.properties_in[0].config.test == "test" + + assert len(m.fs.cv.properties_out[0].config) == 4 + assert m.fs.cv.properties_out[0].config.test == "test" + + +# ----------------------------------------------------------------------------- +# Test add_reaction_blocks +@pytest.mark.unit +def test_add_reaction_blocks(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + assert hasattr(m.fs.cv, "reactions") + assert len(m.fs.cv.reactions[0].config) == 3 + assert m.fs.cv.reactions[0].config.state_block == m.fs.cv.properties_out + assert m.fs.cv.reactions[0].state_ref == m.fs.cv.properties_out[0] + assert m.fs.cv.reactions[0].config.has_equilibrium is False + assert m.fs.cv.reactions[0].config.parameters == m.fs.rp + + +@pytest.mark.unit +def test_add_reaction_blocks_has_equilibrium(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + assert m.fs.cv.reactions[0].config.has_equilibrium is True + + +@pytest.mark.unit +def test_add_reaction_blocks_no_has_equilibrium(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_reaction_blocks() + + +@pytest.mark.unit +def test_add_reaction_blocks_custom_args(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, + reaction_package=m.fs.rp, + reaction_package_args={"test1": 1}, + ) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + assert m.fs.cv.reactions[0].config.test1 == 1 + + +# ----------------------------------------------------------------------------- +# Test _add_phase_fractions +@pytest.mark.unit +def test_add_phase_fractions(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv._add_phase_fractions() + + assert isinstance(m.fs.cv.phase_fraction, Var) + assert len(m.fs.cv.phase_fraction) == 2 + assert hasattr(m.fs.cv, "sum_of_phase_fractions") + + +@pytest.mark.unit +def test_add_phase_fractions_single_phase(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.del_component(m.fs.pp.phase_list) + m.fs.pp.phase_list = Set(initialize=["p1"]) + + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv._add_phase_fractions() + + assert isinstance(m.fs.cv.phase_fraction, Expression) + assert len(m.fs.cv.phase_fraction) == 1 + assert not hasattr(m.fs.cv, "sum_of_phase_fractions") + + +# ----------------------------------------------------------------------------- +# Test reaction rate conversion method +@pytest.mark.unit +def test_rxn_rate_conv_property_basis_other(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 3 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + for t in m.fs.time: + for j in m.fs.pp.component_list: + with pytest.raises(ConfigurationError): + m.fs.cv._rxn_rate_conv(t, j) + + +@pytest.mark.unit +def test_rxn_rate_conv_reaction_basis_other(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.rp.basis_switch = 3 + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + for t in m.fs.time: + for j in m.fs.pp.component_list: + with pytest.raises(ConfigurationError): + m.fs.cv._rxn_rate_conv(t, j) + + +@pytest.mark.unit +def test_rxn_rate_conv_both_molar(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + for t in m.fs.time: + for j in m.fs.pp.component_list: + assert m.fs.cv._rxn_rate_conv(t, j) == 1 + + +@pytest.mark.unit +def test_rxn_rate_conv_both_mass(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.basis_switch = 2 + m.fs.rp.basis_switch = 2 + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + for t in m.fs.time: + for j in m.fs.pp.component_list: + assert m.fs.cv._rxn_rate_conv(t, j) == 1 + + +@pytest.mark.unit +def test_rxn_rate_conv_mole_mass_no_mw(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.basis_switch = 1 + m.fs.rp.basis_switch = 2 + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + for t in m.fs.time: + for j in m.fs.pp.component_list: + with pytest.raises(PropertyNotSupportedError): + m.fs.cv._rxn_rate_conv(t, j) + + +@pytest.mark.unit +def test_rxn_rate_conv_mass_mole_no_mw(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.basis_switch = 2 + m.fs.rp.basis_switch = 1 + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + for t in m.fs.time: + for j in m.fs.pp.component_list: + with pytest.raises(PropertyNotSupportedError): + m.fs.cv._rxn_rate_conv(t, j) + + +@pytest.mark.unit +def test_rxn_rate_conv_mole_mass(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.basis_switch = 1 + m.fs.rp.basis_switch = 2 + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = {"c1": 2, "c2": 3} + for j in m.fs.pp.component_list: + assert ( + m.fs.cv._rxn_rate_conv(t, j) == 1 / m.fs.cv.properties_out[t].mw_comp[j] + ) + + +@pytest.mark.unit +def test_rxn_rate_conv_mass_mole(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.basis_switch = 2 + m.fs.rp.basis_switch = 1 + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = {"c1": 2, "c2": 3} + for j in m.fs.pp.component_list: + assert m.fs.cv._rxn_rate_conv(t, j) == m.fs.cv.properties_out[t].mw_comp[j] + + +# ----------------------------------------------------------------------------- +# Test add_material_balances default +@pytest.mark.unit +def test_add_material_balances_default_fail(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.pp.default_balance_switch = 2 + + with pytest.raises(ConfigurationError): + m.fs.cv.add_material_balances(MaterialBalanceType.useDefault) + + +@pytest.mark.unit +def test_add_material_balances_default(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_material_balances(MaterialBalanceType.useDefault) + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_material_balances_rxn_molar(): + # use property package with mass basis to confirm correct rxn term units + # add options so that all generation/extent terms exist + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.rp.basis_switch = 1 + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + units = m.fs.cv.config.property_package.get_metadata().get_derived_units + pp_units = units("flow_mass") # basis 2 is mass + rp_units = units("flow_mole") # basis 1 is molar + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + # add molecular weight variable to each time point, using correct units + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = Var( + m.fs.cv.properties_out[t].config.parameters.component_list, + units=units("mass") / units("amount"), + ) + + # add material balances to control volume + m.fs.cv.add_material_balances( + balance_type=MaterialBalanceType.componentPhase, + has_rate_reactions=True, + has_equilibrium_reactions=True, + has_phase_equilibrium=True, + ) + + assert_units_equivalent(m.fs.cv.rate_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.rate_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_generation, pp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_extent, pp_units) + assert_units_equivalent(m.fs.cv.phase_equilibrium_generation, pp_units) + + +@pytest.mark.unit +def test_add_material_balances_rxn_mass(): + # use property package with mass basis to confirm correct rxn term units + # add options so that all generation/extent terms exist + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + + m.fs.pp.basis_switch = 1 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.rp.basis_switch = 2 + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + units = m.fs.cv.config.property_package.get_metadata().get_derived_units + pp_units = units("flow_mole") # basis 2 is molar + rp_units = units("flow_mass") # basis 1 is mass + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + # add molecular weight variable to each time point, using correct units + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = Var( + m.fs.cv.properties_out[t].config.parameters.component_list, + units=units("mass") / units("amount"), + ) + + # add material balances to control volume + m.fs.cv.add_material_balances( + balance_type=MaterialBalanceType.componentPhase, + has_rate_reactions=True, + has_equilibrium_reactions=True, + has_phase_equilibrium=True, + ) + + assert_units_equivalent(m.fs.cv.rate_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.rate_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_generation, rp_units) + assert_units_equivalent(m.fs.cv.equilibrium_reaction_extent, rp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_generation, pp_units) + assert_units_equivalent(m.fs.cv.inherent_reaction_extent, pp_units) + assert_units_equivalent(m.fs.cv.phase_equilibrium_generation, pp_units) + + +@pytest.mark.unit +def test_add_material_balances_single_phase_w_equilibrium(caplog): + from idaes.models.properties import iapws95 + + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = iapws95.Iapws95ParameterBlock( + phase_presentation=iapws95.PhaseType.MIX, state_vars=iapws95.StateVars.PH + ) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + m.fs.cv.add_material_balances( + balance_type=MaterialBalanceType.useDefault, + has_phase_equilibrium=True, + ) + msg = ( + "DEPRECATED: Property package has only one phase; control volume cannot " + "include phase equilibrium terms. Some property packages support phase " + "equilibrium implicitly in which case additional terms are not " + "necessary. You should set has_phase_equilibrium=False. (deprecated in " + "2.0.0, will be removed in (or after) 3.0.0)" + ) + assert msg.replace(" ", "") in caplog.records[0].message.replace("\n", "").replace( + " ", "" + ) + + +# ----------------------------------------------------------------------------- +# Test add_phase_component_balances +@pytest.mark.unit +def test_add_phase_component_balances_default(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_phase_component_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_dynamic(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True + ) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_phase_component_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 8 + assert isinstance(m.fs.cv.phase_fraction, Var) + assert isinstance(m.fs.cv.material_holdup, Var) + assert isinstance(m.fs.cv.material_accumulation, Var) + + assert_units_consistent(m) + assert_units_equivalent(m.fs.cv.material_holdup, units.mol) + assert_units_equivalent(m.fs.cv.material_accumulation, units.mol / units.s) + + +@pytest.mark.unit +def test_add_phase_component_balances_dynamic_no_geometry(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True + ) + + # Do not add geometry + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_phase_component_balances() + + +@pytest.mark.unit +def test_add_phase_component_balances_rate_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_phase_component_balances(has_rate_reactions=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + assert isinstance(m.fs.cv.rate_reaction_generation, Var) + assert isinstance(m.fs.cv.rate_reaction_extent, Var) + assert isinstance(m.fs.cv.rate_reaction_stoichiometry_constraint, Constraint) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_rate_rxns_no_ReactionBlock(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_phase_component_balances(has_rate_reactions=True) + + +@pytest.mark.unit +def test_add_phase_component_balances_rate_rxns_no_rxn_idx(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.rp.del_component(m.fs.rp.rate_reaction_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_phase_component_balances(has_rate_reactions=True) + + +@pytest.mark.unit +def test_add_phase_component_balances_eq_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + mb = m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + assert isinstance(m.fs.cv.equilibrium_reaction_generation, Var) + assert isinstance(m.fs.cv.equilibrium_reaction_extent, Var) + assert isinstance(m.fs.cv.equilibrium_reaction_stoichiometry_constraint, Constraint) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_eq_rxns_not_active(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) + + +@pytest.mark.unit +def test_add_phase_component_balances_eq_rxns_no_idx(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.rp.del_component(m.fs.rp.equilibrium_reaction_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) + + +@pytest.mark.unit +def test_add_phase_component_balances_in_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + mb = m.fs.cv.add_phase_component_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + assert isinstance(m.fs.cv.inherent_reaction_generation, Var) + assert isinstance(m.fs.cv.inherent_reaction_extent, Var) + assert isinstance(m.fs.cv.inherent_reaction_stoichiometry_constraint, Constraint) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_in_rxns_no_idx(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + # delete inherent_Reaction_dix to trigger exception + m.fs.pp.del_component(m.fs.pp.inherent_reaction_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + with pytest.raises( + PropertyNotSupportedError, + match=r"fs.cv Property package does not contain a " + r"list of inherent reactions \(inherent_reaction_idx\), " + r"but include_inherent_reactions is True.", + ): + m.fs.cv.add_phase_component_balances() + + +@pytest.mark.unit +def test_add_phase_component_balances_eq_rxns_no_ReactionBlock(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) + + +@pytest.mark.unit +def test_add_phase_component_balances_phase_eq(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_phase_component_balances(has_phase_equilibrium=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + assert isinstance(m.fs.cv.phase_equilibrium_generation, Var) + assert isinstance(m.fs.cv.config.property_package.phase_equilibrium_idx, Set) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_phase_eq_not_active(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_phase_component_balances(has_phase_equilibrium=True) + + +@pytest.mark.unit +def test_add_phase_component_balances_phase_eq_no_idx(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_phase_component_balances(has_phase_equilibrium=True) + + +@pytest.mark.unit +def test_add_phase_component_balances_mass_transfer(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_phase_component_balances(has_mass_transfer=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + assert isinstance(m.fs.cv.mass_transfer_term, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_custom_molar_term(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, + m.fs.cv.config.property_package.phase_list, + m.fs.cv.config.property_package.component_list, + ) + + def custom_method(t, p, j): + return m.fs.cv.test_var[t, p, j] * units.mol / units.s + + mb = m.fs.cv.add_phase_component_balances(custom_molar_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_custom_molar_term_no_mw(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, + m.fs.cv.config.property_package.phase_list, + m.fs.cv.config.property_package.component_list, + ) + + def custom_method(t, p, j): + return m.fs.cv.test_var[t, p, j] + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_phase_component_balances(custom_molar_term=custom_method) + + +@pytest.mark.unit +def test_add_phase_component_balances_custom_molar_term_mass_flow_basis(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, + m.fs.cv.config.property_package.phase_list, + m.fs.cv.config.property_package.component_list, + ) + + def custom_method(t, p, j): + return m.fs.cv.test_var[t, p, j] * units.mol / units.s + + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = Var( + m.fs.cv.properties_out[t].config.parameters.component_list, + units=units.kg / units.mol, + ) + + mb = m.fs.cv.add_phase_component_balances(custom_molar_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_custom_molar_term_undefined_basis(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 3 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, + m.fs.cv.config.property_package.phase_list, + m.fs.cv.config.property_package.component_list, + ) + + def custom_method(t, p, j): + return m.fs.cv.test_var[t, p, j] + + with pytest.raises(ConfigurationError): + m.fs.cv.add_phase_component_balances(custom_molar_term=custom_method) + + +@pytest.mark.unit +def test_add_phase_component_balances_custom_mass_term(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, + m.fs.cv.config.property_package.phase_list, + m.fs.cv.config.property_package.component_list, + ) + + def custom_method(t, p, j): + return m.fs.cv.test_var[t, p, j] * units.kg / units.s + + mb = m.fs.cv.add_phase_component_balances(custom_mass_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_custom_mass_term_no_mw(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 1 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, + m.fs.cv.config.property_package.phase_list, + m.fs.cv.config.property_package.component_list, + ) + + def custom_method(t, p, j): + return m.fs.cv.test_var[t, p, j] + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_phase_component_balances(custom_mass_term=custom_method) + + +@pytest.mark.unit +def test_add_phase_component_balances_custom_mass_term_mole_flow_basis(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, + m.fs.cv.config.property_package.phase_list, + m.fs.cv.config.property_package.component_list, + ) + + def custom_method(t, p, j): + return m.fs.cv.test_var[t, p, j] * units.kg / units.s + + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = Var( + m.fs.cv.properties_out[t].config.parameters.component_list, + units=units.kg / units.mol, + ) + + mb = m.fs.cv.add_phase_component_balances(custom_mass_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_phase_component_balances_custom_mass_term_undefined_basis(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 3 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, + m.fs.cv.config.property_package.phase_list, + m.fs.cv.config.property_package.component_list, + ) + + def custom_method(t, p, j): + return m.fs.cv.test_var[t, p, j] + + with pytest.raises(ConfigurationError): + m.fs.cv.add_phase_component_balances(custom_mass_term=custom_method) + + +# ----------------------------------------------------------------------------- +# Test add_total_component_balances +@pytest.mark.unit +def test_add_total_component_balances_default(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_component_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_dynamic(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True + ) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_component_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 4 + assert isinstance(m.fs.cv.phase_fraction, Var) + assert isinstance(m.fs.cv.material_holdup, Var) + assert isinstance(m.fs.cv.material_accumulation, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_dynamic_no_geometry(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True + ) + + # Do not add geometry + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_component_balances() + + +@pytest.mark.unit +def test_add_total_component_balances_rate_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_component_balances(has_rate_reactions=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + assert isinstance(m.fs.cv.rate_reaction_generation, Var) + assert isinstance(m.fs.cv.rate_reaction_extent, Var) + assert isinstance(m.fs.cv.rate_reaction_stoichiometry_constraint, Constraint) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_rate_rxns_no_ReactionBlock(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_component_balances(has_rate_reactions=True) + + +@pytest.mark.unit +def test_add_total_component_balances_rate_rxns_no_rxn_idx(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.rp.del_component(m.fs.rp.rate_reaction_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_total_component_balances(has_rate_reactions=True) + + +@pytest.mark.unit +def test_add_total_component_balances_eq_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + mb = m.fs.cv.add_total_component_balances(has_equilibrium_reactions=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + assert isinstance(m.fs.cv.equilibrium_reaction_generation, Var) + assert isinstance(m.fs.cv.equilibrium_reaction_extent, Var) + assert isinstance(m.fs.cv.equilibrium_reaction_stoichiometry_constraint, Constraint) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_eq_rxns_not_active(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_component_balances(has_equilibrium_reactions=True) + + +@pytest.mark.unit +def test_add_total_component_balances_eq_rxns_no_idx(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.rp.del_component(m.fs.rp.equilibrium_reaction_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_total_component_balances(has_equilibrium_reactions=True) + + +@pytest.mark.unit +def test_add_total_component_balances_eq_rxns_no_ReactionBlock(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_component_balances(has_equilibrium_reactions=True) + + +@pytest.mark.unit +def test_add_total_component_balances_in_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + mb = m.fs.cv.add_total_component_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + assert isinstance(m.fs.cv.inherent_reaction_generation, Var) + assert isinstance(m.fs.cv.inherent_reaction_extent, Var) + assert isinstance(m.fs.cv.inherent_reaction_stoichiometry_constraint, Constraint) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_in_rxns_no_idx(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + # Set property package to contain inherent reactions + m.fs.pp._has_inherent_reactions = True + # delete inherent_Reaction_dix to trigger exception + m.fs.pp.del_component(m.fs.pp.inherent_reaction_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + with pytest.raises( + PropertyNotSupportedError, + match=r"fs.cv Property package does not contain a " + r"list of inherent reactions \(inherent_reaction_idx\), " + r"but include_inherent_reactions is True.", + ): + m.fs.cv.add_total_component_balances() + + +@pytest.mark.unit +def test_add_total_component_balances_phase_eq_not_active(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_component_balances(has_phase_equilibrium=True) + + +@pytest.mark.unit +def test_add_total_component_balances_mass_transfer(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_component_balances(has_mass_transfer=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + assert isinstance(m.fs.cv.mass_transfer_term, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_custom_molar_term(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list + ) + + def custom_method(t, j): + return m.fs.cv.test_var[t, j] * units.mol / units.s + + mb = m.fs.cv.add_total_component_balances(custom_molar_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_custom_molar_term_no_mw(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list + ) + + def custom_method(t, j): + return m.fs.cv.test_var[t, j] + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_total_component_balances(custom_molar_term=custom_method) + + +@pytest.mark.unit +def test_add_total_component_balances_custom_molar_term_mass_flow_basis(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list + ) + + def custom_method(t, j): + return m.fs.cv.test_var[t, j] * units.mol / units.s + + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = Var( + m.fs.cv.properties_out[t].config.parameters.component_list, + units=units.kg / units.mol, + ) + + mb = m.fs.cv.add_total_component_balances(custom_molar_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_custom_molar_term_undefined_basis(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 3 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list + ) + + def custom_method(t, j): + return m.fs.cv.test_var[t, j] + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_component_balances(custom_molar_term=custom_method) + + +@pytest.mark.unit +def test_add_total_component_balances_custom_mass_term(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list + ) + + def custom_method(t, j): + return m.fs.cv.test_var[t, j] * units.kg / units.s + + mb = m.fs.cv.add_total_component_balances(custom_mass_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_custom_mass_term_no_mw(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 1 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list + ) + + def custom_method(t, j): + return m.fs.cv.test_var[t, j] + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_total_component_balances(custom_mass_term=custom_method) + + +@pytest.mark.unit +def test_add_total_component_balances_custom_mass_term_mole_flow_basis(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 2 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list + ) + + def custom_method(t, j): + return m.fs.cv.test_var[t, j] * units.kg / units.s + + for t in m.fs.time: + m.fs.cv.properties_out[t].mw_comp = Var( + m.fs.cv.properties_out[t].config.parameters.component_list, + units=units.kg / units.mol, + ) + + mb = m.fs.cv.add_total_component_balances(custom_mass_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_component_balances_custom_mass_term_undefined_basis(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.pp.basis_switch = 3 + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var( + m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list + ) + + def custom_method(t, j): + return m.fs.cv.test_var[t, j] + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_component_balances(custom_mass_term=custom_method) + + +# ----------------------------------------------------------------------------- +# Test add_total_element_balances +@pytest.mark.unit +def test_add_total_element_balances_default(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_element_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 3 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_element_balances_properties_not_supported(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.element_list) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(PropertyNotSupportedError): + m.fs.cv.add_total_element_balances() + + +@pytest.mark.unit +def test_add_total_element_balances_dynamic(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True + ) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_element_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 6 + assert isinstance(m.fs.cv.phase_fraction, Var) + assert isinstance(m.fs.cv.element_holdup, Var) + assert isinstance(m.fs.cv.element_accumulation, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_element_balances_dynamic_no_geometry(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True + ) + + # Do not add geometry + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_element_balances() + + +@pytest.mark.unit +def test_add_total_element_balances_rate_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_element_balances(has_rate_reactions=True) + + +@pytest.mark.unit +def test_add_total_element_balances_eq_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_element_balances(has_equilibrium_reactions=True) + + +@pytest.mark.unit +def test_add_total_element_balances_phase_eq(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_element_balances(has_phase_equilibrium=True) + + +@pytest.mark.unit +def test_add_total_element_balances_mass_transfer(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_element_balances(has_mass_transfer=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 3 + assert isinstance(m.fs.cv.elemental_mass_transfer_term, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_element_balances_custom_term(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var(m.fs.cv.flowsheet().time, m.fs.pp.element_list) + + def custom_method(t, e): + return m.fs.cv.test_var[t, e] * units.mol / units.s + + mb = m.fs.cv.add_total_element_balances(custom_elemental_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 3 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_element_balances_lineraly_dependent(caplog): + caplog.set_level(idaeslog.INFO_LOW) + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + # Change elemental composition to introduce dependency + m.fs.pp.element_comp = { + "c1": {"H": 0, "He": 0, "Li": 1}, + "c2": {"H": 1, "He": 2, "Li": 0}, + } + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + mb = m.fs.cv.add_total_element_balances() + # Check that logger message was recorded and has the right level + msg = ( + "fs.cv detected linearly dependent element balance equations. " + "Element balances will NOT be written for the following elements: " + "['He']" + ) + assert msg in caplog.text + for record in caplog.records: + if "['He']" in record.msg: + assert record.levelno == idaeslog.INFO_LOW + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + for e in mb: + # H and Li are not lineraly dependent and should have constraints + assert e in [(0, "H"), (0, "Li")] + # He is lineraly dependent on H and should be skipped + + assert_units_consistent(m) + + +# ----------------------------------------------------------------------------- +# Test unsupported material balance types +@pytest.mark.unit +def test_add_total_material_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_total_material_balances() + + +# ----------------------------------------------------------------------------- +# Test add_energy_balances default +@pytest.mark.unit +def test_add_energy_balances_default_fail(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.pp.default_balance_switch = 2 + + with pytest.raises(ConfigurationError): + m.fs.cv.add_energy_balances(EnergyBalanceType.useDefault) + + +@pytest.mark.unit +def test_add_energy_balances_default(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + eb = m.fs.cv.add_energy_balances(EnergyBalanceType.useDefault) + + assert isinstance(eb, Constraint) + assert len(eb) == 1 + + assert_units_consistent(m) + + +# ----------------------------------------------------------------------------- +# Test phase enthalpy balances +@pytest.mark.unit +def test_add_total_enthalpy_balances_default(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + eb = m.fs.cv.add_total_enthalpy_balances() + + assert isinstance(eb, Constraint) + assert len(eb) == 1 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_enthalpy_balances_dynamic(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True + ) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_enthalpy_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 2 + assert isinstance(m.fs.cv.phase_fraction, Var) + assert isinstance(m.fs.cv.energy_holdup, Var) + assert isinstance(m.fs.cv.energy_accumulation, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_enthalpy_balances_dynamic_no_geometry(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True + ) + + # Do not add geometry + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_enthalpy_balances() + + +@pytest.mark.unit +def test_add_total_enthalpy_balances_heat_transfer(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_enthalpy_balances(has_heat_transfer=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 1 + assert isinstance(m.fs.cv.heat, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_enthalpy_balances_work_transfer(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_enthalpy_balances(has_work_transfer=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 1 + assert isinstance(m.fs.cv.work, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_enthalpy_balances_enthalpy_transfer(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_enthalpy_balances(has_enthalpy_transfer=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 1 + assert isinstance(m.fs.cv.enthalpy_transfer, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_enthalpy_balances_custom_term(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var(m.fs.cv.flowsheet().time) + + def custom_method(t): + return m.fs.cv.test_var[t] * units.J / units.s + + mb = m.fs.cv.add_total_enthalpy_balances(custom_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 1 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_enthalpy_balances_dh_rxn_no_extents(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(ConfigurationError): + m.fs.cv.add_total_enthalpy_balances(has_heat_of_reaction=True) + + +@pytest.mark.unit +def test_add_total_enthalpy_balances_dh_rxn_rate_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + m.fs.cv.add_phase_component_balances(has_rate_reactions=True) + + m.fs.cv.add_total_enthalpy_balances(has_heat_of_reaction=True) + assert isinstance(m.fs.cv.heat_of_reaction, Expression) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_enthalpy_balances_dh_rxn_equil_rxns(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) + + m.fs.cv.add_total_enthalpy_balances(has_heat_of_reaction=True) + assert isinstance(m.fs.cv.heat_of_reaction, Expression) + + assert_units_consistent(m) + + +# ----------------------------------------------------------------------------- +# Test unsupported energy balance types +@pytest.mark.unit +def test_add_phase_enthalpy_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_phase_enthalpy_balances() + + +@pytest.mark.unit +def test_add_phase_energy_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_phase_energy_balances() + + +@pytest.mark.unit +def test_add_total_energy_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_total_energy_balances() + + +@pytest.mark.unit +def test_add_isothermal_energy_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_isothermal_constraint() + + +# ----------------------------------------------------------------------------- +# Test add total pressure balances +@pytest.mark.unit +def test_add_total_pressure_balances_default(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + eb = m.fs.cv.add_total_pressure_balances() + + assert isinstance(eb, Constraint) + assert len(eb) == 1 + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_pressure_balances_deltaP(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_total_pressure_balances(has_pressure_change=True) + + assert isinstance(mb, Constraint) + assert len(mb) == 1 + assert isinstance(m.fs.cv.deltaP, Var) + + assert_units_consistent(m) + + +@pytest.mark.unit +def test_add_total_pressure_balances_custom_term(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.test_var = Var(m.fs.cv.flowsheet().time) + + def custom_method(t): + return m.fs.cv.test_var[t] * units.Pa + + mb = m.fs.cv.add_total_pressure_balances(custom_term=custom_method) + + assert isinstance(mb, Constraint) + assert len(mb) == 1 + + assert_units_consistent(m) + + +# ----------------------------------------------------------------------------- +# Test unsupported momentum balance types +@pytest.mark.unit +def test_add_phase_pressure_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_phase_pressure_balances() + + +@pytest.mark.unit +def test_add_phase_momentum_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_phase_momentum_balances() + + +@pytest.mark.unit +def test_add_total_momentum_balances(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + with pytest.raises(BalanceTypeNotSupportedError): + m.fs.cv.add_total_momentum_balances() + + +# ----------------------------------------------------------------------------- +# Test model checks, initialize and release_state +@pytest.mark.unit +def test_model_checks(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + m.fs.cv.model_check() + + for t in m.fs.time: + assert m.fs.cv.properties_in[t].check is True + assert m.fs.cv.properties_out[t].check is True + assert m.fs.cv.reactions[t].check is True + + +@pytest.mark.unit +def test_initialize(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + f = m.fs.cv.initialize() + + for t in m.fs.time: + assert m.fs.cv.properties_in[t].init_test is True + assert m.fs.cv.properties_out[t].init_test is True + assert m.fs.cv.properties_in[t].hold_state is True + assert m.fs.cv.properties_out[t].hold_state is False + assert m.fs.cv.reactions[t].init_test is True + + m.fs.cv.release_state(flags=f) + + for t in m.fs.time: + assert m.fs.cv.properties_in[t].hold_state is False + assert m.fs.cv.properties_out[t].hold_state is False + + +@pytest.mark.unit +def test_get_stream_table_contents(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + + df = m.fs.cv._get_stream_table_contents() + + assert df.loc["component_flow_phase ('p1', 'c1')"]["In"] == 2 + assert df.loc["component_flow_phase ('p1', 'c2')"]["In"] == 2 + assert df.loc["component_flow_phase ('p2', 'c1')"]["In"] == 2 + assert df.loc["component_flow_phase ('p2', 'c2')"]["In"] == 2 + assert df.loc["pressure"]["In"] == 1e5 + assert df.loc["temperature"]["In"] == 300 + + assert df.loc["component_flow_phase ('p1', 'c1')"]["Out"] == 2 + assert df.loc["component_flow_phase ('p1', 'c2')"]["Out"] == 2 + assert df.loc["component_flow_phase ('p2', 'c1')"]["Out"] == 2 + assert df.loc["component_flow_phase ('p2', 'c2')"]["Out"] == 2 + assert df.loc["pressure"]["Out"] == 1e5 + assert df.loc["temperature"]["Out"] == 300 + + +@pytest.mark.unit +def test_get_performance_contents(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + m.fs.cv.add_material_balances( + has_rate_reactions=True, + has_equilibrium_reactions=True, + has_phase_equilibrium=True, + has_mass_transfer=True, + ) + m.fs.cv.add_energy_balances( + has_heat_of_reaction=True, has_work_transfer=True, has_heat_transfer=True + ) + m.fs.cv.add_momentum_balances(has_pressure_change=True) + + dd = m.fs.cv._get_performance_contents() + + assert len(dd) == 3 + for k in dd.keys(): + assert k in ("vars", "exprs", "params") + assert len(dd["vars"]) == 36 + for k in dd["vars"].keys(): + assert k in [ + "Volume", + "Heat Transfer", + "Work Transfer", + "Pressure Change", + "Phase Fraction [p1]", + "Phase Fraction [p2]", + "Energy Holdup [p1]", + "Energy Holdup [p2]", + "Energy Accumulation [p1]", + "Energy Accumulation [p2]", + "Material Holdup [p1, c1]", + "Material Holdup [p1, c2]", + "Material Holdup [p2, c1]", + "Material Holdup [p2, c2]", + "Material Accumulation [p1, c1]", + "Material Accumulation [p1, c2]", + "Material Accumulation [p2, c1]", + "Material Accumulation [p2, c2]", + "Rate Reaction Generation [p1, c1]", + "Rate Reaction Generation [p1, c2]", + "Rate Reaction Generation [p2, c1]", + "Rate Reaction Generation [p2, c2]", + "Equilibrium Reaction Generation [p1, c1]", + "Equilibrium Reaction Generation [p1, c2]", + "Equilibrium Reaction Generation [p2, c1]", + "Equilibrium Reaction Generation [p2, c2]", + "Mass Transfer Term [p1, c1]", + "Mass Transfer Term [p1, c2]", + "Mass Transfer Term [p2, c1]", + "Mass Transfer Term [p2, c2]", + "Rate Reaction Extent [r1]", + "Rate Reaction Extent [r2]", + "Equilibrium Reaction Extent [e1]", + "Equilibrium Reaction Extent [e2]", + "Phase Equilibrium Generation [e1]", + "Phase Equilibrium Generation [e2]", + ] + + assert len(dd["exprs"]) == 1 + for k in dd["exprs"].keys(): + assert k in ["Heat of Reaction Term"] + + assert len(dd["params"]) == 0 + + +@pytest.mark.unit +def test_get_performance_contents_elemental(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + m.fs.cv.add_total_element_balances(has_mass_transfer=True) + m.fs.cv.add_energy_balances( + has_heat_of_reaction=False, has_work_transfer=True, has_heat_transfer=True + ) + m.fs.cv.add_momentum_balances(has_pressure_change=True) + + dd = m.fs.cv._get_performance_contents() + + assert len(dd) == 3 + for k in dd.keys(): + assert k in ("vars", "exprs", "params") + assert len(dd["vars"]) == 19 + for k in dd["vars"].keys(): + assert k in [ + "Volume", + "Heat Transfer", + "Work Transfer", + "Pressure Change", + "Phase Fraction [p1]", + "Phase Fraction [p2]", + "Energy Holdup [p1]", + "Energy Holdup [p2]", + "Energy Accumulation [p1]", + "Energy Accumulation [p2]", + "Elemental Holdup [H]", + "Elemental Holdup [He]", + "Elemental Holdup [Li]", + "Elemental Accumulation [H]", + "Elemental Accumulation [He]", + "Elemental Accumulation [Li]", + "Elemental Transfer Term [H]", + "Elemental Transfer Term [He]", + "Elemental Transfer Term [Li]", + ] + + assert len(dd["exprs"]) == 12 + for k in dd["exprs"].keys(): + assert k in [ + "Element Flow In [p1, H]", + "Element Flow In [p1, He]", + "Element Flow In [p1, Li]", + "Element Flow In [p2, H]", + "Element Flow In [p2, He]", + "Element Flow In [p2, Li]", + "Element Flow Out [p1, H]", + "Element Flow Out [p1, He]", + "Element Flow Out [p1, Li]", + "Element Flow Out [p2, H]", + "Element Flow Out [p2, He]", + "Element Flow Out [p2, Li]", + ] + + assert len(dd["params"]) == 0 + + +@pytest.mark.unit +def test_reports(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=True) + m.fs.cv.add_reaction_blocks(has_equilibrium=True) + m.fs.cv.add_material_balances( + has_rate_reactions=True, + has_equilibrium_reactions=True, + has_phase_equilibrium=True, + ) + m.fs.cv.add_energy_balances(has_heat_of_reaction=True, has_heat_transfer=True) + m.fs.cv.add_momentum_balances(has_pressure_change=True) + + m.fs.cv.report() + + +@pytest.mark.unit +def test_dynamic_mass_basis(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) + m.fs.pp.basis_switch = 2 + m.fs.cv = ControlVolume0DBlock( + property_package=m.fs.pp, + reaction_package=m.fs.rp, + dynamic=True, + ) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv.add_reaction_blocks(has_equilibrium=False) + + mb = m.fs.cv.add_phase_component_balances() + + assert isinstance(mb, Constraint) + assert len(mb) == 8 + assert isinstance(m.fs.cv.phase_fraction, Var) + assert isinstance(m.fs.cv.material_holdup, Var) + assert isinstance(m.fs.cv.material_accumulation, Var) + + assert_units_consistent(m) + assert_units_equivalent(m.fs.cv.material_holdup, units.kg) + assert_units_equivalent(m.fs.cv.material_accumulation, units.kg / units.s) From 93c41b8c43f2f15376a8237852f5187dc34cb6b7 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 15 Jan 2025 11:18:00 -0500 Subject: [PATCH 02/14] Extended CV with isothermal option --- idaes/core/base/extended_control_volume0d.py | 4 +- idaes/core/base/extended_control_volume1d.py | 2 +- .../tests/test_extended_control_volume_0d.py | 2661 +---------------- .../tests/test_extended_control_volume_1d.py | 203 ++ 4 files changed, 268 insertions(+), 2602 deletions(-) create mode 100644 idaes/core/base/tests/test_extended_control_volume_1d.py diff --git a/idaes/core/base/extended_control_volume0d.py b/idaes/core/base/extended_control_volume0d.py index 9597917e0b..7b711b6607 100644 --- a/idaes/core/base/extended_control_volume0d.py +++ b/idaes/core/base/extended_control_volume0d.py @@ -90,7 +90,7 @@ def add_isothermal_constraint( ) if has_enthalpy_transfer: raise ConfigurationError( - f"{self.name}: isothermal energy balance option does not support enthalpy transfer. " + f"{self.name}: isothermal energy balance option does not support enthalpy transfer." ) if has_heat_of_reaction: raise ConfigurationError( @@ -100,7 +100,7 @@ def add_isothermal_constraint( ) if custom_term is not None: raise ConfigurationError( - f"{self.name}: isothermal energy balance option does not support custom terms. " + f"{self.name}: isothermal energy balance option does not support custom terms." ) # Add isothermal constraint diff --git a/idaes/core/base/extended_control_volume1d.py b/idaes/core/base/extended_control_volume1d.py index 3d35cd952c..6dee23eed3 100644 --- a/idaes/core/base/extended_control_volume1d.py +++ b/idaes/core/base/extended_control_volume1d.py @@ -104,7 +104,7 @@ def add_isothermal_constraint( ) # Add isothermal constraint - @self.Constraint(self.flowsheet().time, doc="Energy balances") + @self.Constraint(self.flowsheet().time, self.length_domain, doc="Energy balances") def enthalpy_balances(b, t, x): if ( b.config.transformation_scheme != "FORWARD" diff --git a/idaes/core/base/tests/test_extended_control_volume_0d.py b/idaes/core/base/tests/test_extended_control_volume_0d.py index 516f1dc2fa..73b8b82590 100644 --- a/idaes/core/base/tests/test_extended_control_volume_0d.py +++ b/idaes/core/base/tests/test_extended_control_volume_0d.py @@ -16,28 +16,20 @@ Author: Andrew Lee """ import pytest -from pyomo.environ import ConcreteModel, Constraint, Expression, Set, units, Var -from pyomo.util.check_units import assert_units_consistent, assert_units_equivalent -from pyomo.common.config import ConfigBlock +from pyomo.environ import ConcreteModel, Constraint, units +from pyomo.util.check_units import assert_units_consistent + from idaes.core import ( - ExtendedControlVolume0DBlock as ControlVolume0DBlock, - ControlVolumeBlockData, + ExtendedControlVolume0DBlock, FlowsheetBlockData, declare_process_block_class, - FlowDirection, - MaterialBalanceType, - EnergyBalanceType, ) from idaes.core.util.exceptions import ( - BalanceTypeNotSupportedError, ConfigurationError, - PropertyNotSupportedError, ) from idaes.core.util.testing import ( PhysicalParameterTestBlock, - ReactionParameterTestBlock, ) -import idaes.logger as idaeslog # ----------------------------------------------------------------------------- @@ -48,2656 +40,127 @@ def build(self): super(_Flowsheet, self).build() -@declare_process_block_class("CVFrame") -class CVFrameData(ControlVolume0DBlock): - def build(self): - super(ControlVolumeBlockData, self).build() - - -# ----------------------------------------------------------------------------- -# Basic tests -@pytest.mark.unit -def test_base_build(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - assert len(m.fs.cv.config) == 7 - assert m.fs.cv.config.dynamic is False - assert m.fs.cv.config.has_holdup is False - assert m.fs.cv.config.property_package == m.fs.pp - assert isinstance(m.fs.cv.config.property_package_args, ConfigBlock) - assert len(m.fs.cv.config.property_package_args) == 0 - assert m.fs.cv.config.reaction_package is None - assert isinstance(m.fs.cv.config.reaction_package_args, ConfigBlock) - assert len(m.fs.cv.config.reaction_package_args) == 0 - assert m.fs.cv.config.auto_construct is False - - assert hasattr(m.fs.config, "time") - - -# ----------------------------------------------------------------------------- -# Test add_geometry @pytest.mark.unit -def test_add_geometry(): +def test_add_isothermal_constraint(): m = ConcreteModel() m.fs = Flowsheet(dynamic=False) m.fs.pp = PhysicalParameterTestBlock() - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) + m.fs.cv = ExtendedControlVolume0DBlock(property_package=m.fs.pp) m.fs.cv.add_geometry() - - assert hasattr(m.fs.cv, "volume") - assert len(m.fs.cv.volume) == 1.0 - assert m.fs.cv.volume[0].value == 1.0 - - -# ----------------------------------------------------------------------------- -# Test add_state_blocks -@pytest.mark.unit -def test_add_state_blocks(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - assert hasattr(m.fs.cv, "properties_in") - assert len(m.fs.cv.properties_in[0].config) == 3 - assert m.fs.cv.properties_in[0].config.defined_state is True - assert m.fs.cv.properties_in[0].config.has_phase_equilibrium is False - assert m.fs.cv.properties_in[0].config.parameters == m.fs.pp - - assert hasattr(m.fs.cv, "properties_out") - assert len(m.fs.cv.properties_out[0].config) == 3 - assert m.fs.cv.properties_out[0].config.defined_state is False - assert m.fs.cv.properties_out[0].config.has_phase_equilibrium is False - assert m.fs.cv.properties_out[0].config.parameters == m.fs.pp - - -@pytest.mark.unit -def test_add_state_block_forward_flow(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks( - information_flow=FlowDirection.forward, has_phase_equilibrium=False - ) - - assert m.fs.cv.properties_in[0].config.defined_state is True - assert m.fs.cv.properties_out[0].config.defined_state is False - - -@pytest.mark.unit -def test_add_state_block_backward_flow(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks( - information_flow=FlowDirection.backward, has_phase_equilibrium=False - ) - - assert m.fs.cv.properties_in[0].config.defined_state is False - assert m.fs.cv.properties_out[0].config.defined_state is True - - -@pytest.mark.unit -def test_add_state_blocks_has_phase_equilibrium(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - - assert m.fs.cv.properties_in[0].config.has_phase_equilibrium is True - assert m.fs.cv.properties_out[0].config.has_phase_equilibrium is True - - -@pytest.mark.unit -def test_add_state_blocks_no_has_phase_equilibrium(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_state_blocks() - - -@pytest.mark.unit -def test_add_state_blocks_custom_args(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, property_package_args={"test": "test"} - ) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - assert len(m.fs.cv.properties_in[0].config) == 4 - assert m.fs.cv.properties_in[0].config.test == "test" - - assert len(m.fs.cv.properties_out[0].config) == 4 - assert m.fs.cv.properties_out[0].config.test == "test" - - -# ----------------------------------------------------------------------------- -# Test add_reaction_blocks -@pytest.mark.unit -def test_add_reaction_blocks(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - assert hasattr(m.fs.cv, "reactions") - assert len(m.fs.cv.reactions[0].config) == 3 - assert m.fs.cv.reactions[0].config.state_block == m.fs.cv.properties_out - assert m.fs.cv.reactions[0].state_ref == m.fs.cv.properties_out[0] - assert m.fs.cv.reactions[0].config.has_equilibrium is False - assert m.fs.cv.reactions[0].config.parameters == m.fs.rp - - -@pytest.mark.unit -def test_add_reaction_blocks_has_equilibrium(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - assert m.fs.cv.reactions[0].config.has_equilibrium is True + m.fs.cv.add_isothermal_constraint() - -@pytest.mark.unit -def test_add_reaction_blocks_no_has_equilibrium(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_reaction_blocks() - - -@pytest.mark.unit -def test_add_reaction_blocks_custom_args(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, - reaction_package=m.fs.rp, - reaction_package_args={"test1": 1}, + assert isinstance(m.fs.cv.enthalpy_balances, Constraint) + assert len(m.fs.cv.enthalpy_balances) == 1 + assert str(m.fs.cv.enthalpy_balances[0].expr) == str( + m.fs.cv.properties_in[0].temperature == m.fs.cv.properties_out[0].temperature ) - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - - assert m.fs.cv.reactions[0].config.test1 == 1 - - -# ----------------------------------------------------------------------------- -# Test _add_phase_fractions -@pytest.mark.unit -def test_add_phase_fractions(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv._add_phase_fractions() - - assert isinstance(m.fs.cv.phase_fraction, Var) - assert len(m.fs.cv.phase_fraction) == 2 - assert hasattr(m.fs.cv, "sum_of_phase_fractions") - - -@pytest.mark.unit -def test_add_phase_fractions_single_phase(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.del_component(m.fs.pp.phase_list) - m.fs.pp.phase_list = Set(initialize=["p1"]) - - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv._add_phase_fractions() - - assert isinstance(m.fs.cv.phase_fraction, Expression) - assert len(m.fs.cv.phase_fraction) == 1 - assert not hasattr(m.fs.cv, "sum_of_phase_fractions") - - -# ----------------------------------------------------------------------------- -# Test reaction rate conversion method -@pytest.mark.unit -def test_rxn_rate_conv_property_basis_other(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 3 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - for t in m.fs.time: - for j in m.fs.pp.component_list: - with pytest.raises(ConfigurationError): - m.fs.cv._rxn_rate_conv(t, j) - - -@pytest.mark.unit -def test_rxn_rate_conv_reaction_basis_other(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.rp.basis_switch = 3 - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - for t in m.fs.time: - for j in m.fs.pp.component_list: - with pytest.raises(ConfigurationError): - m.fs.cv._rxn_rate_conv(t, j) - - -@pytest.mark.unit -def test_rxn_rate_conv_both_molar(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - for t in m.fs.time: - for j in m.fs.pp.component_list: - assert m.fs.cv._rxn_rate_conv(t, j) == 1 - - -@pytest.mark.unit -def test_rxn_rate_conv_both_mass(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.basis_switch = 2 - m.fs.rp.basis_switch = 2 - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - for t in m.fs.time: - for j in m.fs.pp.component_list: - assert m.fs.cv._rxn_rate_conv(t, j) == 1 + assert_units_consistent(m.fs.cv) @pytest.mark.unit -def test_rxn_rate_conv_mole_mass_no_mw(): +def test_add_isothermal_constraint_dynamic(): m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) + m.fs = Flowsheet(dynamic=True, time_set=[0, 1, 2, 3], time_units=units.s) m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.basis_switch = 1 - m.fs.rp.basis_switch = 2 - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + m.fs.cv = ExtendedControlVolume0DBlock(property_package=m.fs.pp) m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - for t in m.fs.time: - for j in m.fs.pp.component_list: - with pytest.raises(PropertyNotSupportedError): - m.fs.cv._rxn_rate_conv(t, j) - - -@pytest.mark.unit -def test_rxn_rate_conv_mass_mole_no_mw(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.basis_switch = 2 - m.fs.rp.basis_switch = 1 - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) + m.fs.cv.add_isothermal_constraint() + assert isinstance(m.fs.cv.enthalpy_balances, Constraint) + assert len(m.fs.cv.enthalpy_balances) == 4 for t in m.fs.time: - for j in m.fs.pp.component_list: - with pytest.raises(PropertyNotSupportedError): - m.fs.cv._rxn_rate_conv(t, j) - - -@pytest.mark.unit -def test_rxn_rate_conv_mole_mass(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.basis_switch = 1 - m.fs.rp.basis_switch = 2 - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) + assert str(m.fs.cv.enthalpy_balances[t].expr) == str( + m.fs.cv.properties_in[t].temperature == m.fs.cv.properties_out[t].temperature + ) - for t in m.fs.time: - m.fs.cv.properties_out[t].mw_comp = {"c1": 2, "c2": 3} - for j in m.fs.pp.component_list: - assert ( - m.fs.cv._rxn_rate_conv(t, j) == 1 / m.fs.cv.properties_out[t].mw_comp[j] - ) + assert_units_consistent(m.fs.cv) @pytest.mark.unit -def test_rxn_rate_conv_mass_mole(): +def test_add_isothermal_constraint_heat_transfer(): m = ConcreteModel() m.fs = Flowsheet(dynamic=False) m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.basis_switch = 2 - m.fs.rp.basis_switch = 1 - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + m.fs.cv = ExtendedControlVolume0DBlock(property_package=m.fs.pp) - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - for t in m.fs.time: - m.fs.cv.properties_out[t].mw_comp = {"c1": 2, "c2": 3} - for j in m.fs.pp.component_list: - assert m.fs.cv._rxn_rate_conv(t, j) == m.fs.cv.properties_out[t].mw_comp[j] + with pytest.raises( + ConfigurationError, + match="fs.cv: isothermal energy balance option requires that has_heat_transfer is False. " + "If you are trying to solve for heat duty to achieve isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ): + m.fs.cv.add_isothermal_constraint(has_heat_transfer=True) -# ----------------------------------------------------------------------------- -# Test add_material_balances default @pytest.mark.unit -def test_add_material_balances_default_fail(): +def test_add_isothermal_constraint_work_transfer(): m = ConcreteModel() m.fs = Flowsheet(dynamic=False) m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - m.fs.pp.default_balance_switch = 2 + m.fs.cv = ExtendedControlVolume0DBlock(property_package=m.fs.pp) - with pytest.raises(ConfigurationError): - m.fs.cv.add_material_balances(MaterialBalanceType.useDefault) + with pytest.raises( + ConfigurationError, + match="fs.cv: isothermal energy balance option requires that has_work_transfer is False. " + "If you are trying to solve for work under isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ): + m.fs.cv.add_isothermal_constraint(has_work_transfer=True) @pytest.mark.unit -def test_add_material_balances_default(): +def test_add_isothermal_constraint_enthalpy_transfer(): m = ConcreteModel() m.fs = Flowsheet(dynamic=False) m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_material_balances(MaterialBalanceType.useDefault) + m.fs.cv = ExtendedControlVolume0DBlock(property_package=m.fs.pp) - assert isinstance(mb, Constraint) - assert len(mb) == 4 - - assert_units_consistent(m) + with pytest.raises( + ConfigurationError, + match="fs.cv: isothermal energy balance option does not support enthalpy transfer." + ): + m.fs.cv.add_isothermal_constraint(has_enthalpy_transfer=True) @pytest.mark.unit -def test_add_material_balances_rxn_molar(): - # use property package with mass basis to confirm correct rxn term units - # add options so that all generation/extent terms exist +def test_add_isothermal_constraint_heat_of_rxn(): m = ConcreteModel() m.fs = Flowsheet(dynamic=False) m.fs.pp = PhysicalParameterTestBlock() - # Set property package to contain inherent reactions - m.fs.pp._has_inherent_reactions = True - - m.fs.pp.basis_switch = 2 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.rp.basis_switch = 1 - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + m.fs.cv = ExtendedControlVolume0DBlock(property_package=m.fs.pp) - units = m.fs.cv.config.property_package.get_metadata().get_derived_units - pp_units = units("flow_mass") # basis 2 is mass - rp_units = units("flow_mole") # basis 1 is molar - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - - # add molecular weight variable to each time point, using correct units - for t in m.fs.time: - m.fs.cv.properties_out[t].mw_comp = Var( - m.fs.cv.properties_out[t].config.parameters.component_list, - units=units("mass") / units("amount"), - ) - - # add material balances to control volume - m.fs.cv.add_material_balances( - balance_type=MaterialBalanceType.componentPhase, - has_rate_reactions=True, - has_equilibrium_reactions=True, - has_phase_equilibrium=True, - ) - - assert_units_equivalent(m.fs.cv.rate_reaction_generation, rp_units) - assert_units_equivalent(m.fs.cv.rate_reaction_extent, rp_units) - assert_units_equivalent(m.fs.cv.equilibrium_reaction_generation, rp_units) - assert_units_equivalent(m.fs.cv.equilibrium_reaction_extent, rp_units) - assert_units_equivalent(m.fs.cv.inherent_reaction_generation, pp_units) - assert_units_equivalent(m.fs.cv.inherent_reaction_extent, pp_units) - assert_units_equivalent(m.fs.cv.phase_equilibrium_generation, pp_units) + with pytest.raises( + ConfigurationError, + match="fs.cv: isothermal energy balance option requires that has_heat_of_reaction is False. " + "If you are trying to solve for heat duty to achieve isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ): + m.fs.cv.add_isothermal_constraint(has_heat_of_reaction=True) @pytest.mark.unit -def test_add_material_balances_rxn_mass(): - # use property package with mass basis to confirm correct rxn term units - # add options so that all generation/extent terms exist +def test_add_isothermal_constraint_custom_term(): m = ConcreteModel() m.fs = Flowsheet(dynamic=False) m.fs.pp = PhysicalParameterTestBlock() - # Set property package to contain inherent reactions - m.fs.pp._has_inherent_reactions = True - - m.fs.pp.basis_switch = 1 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.rp.basis_switch = 2 - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - units = m.fs.cv.config.property_package.get_metadata().get_derived_units - pp_units = units("flow_mole") # basis 2 is molar - rp_units = units("flow_mass") # basis 1 is mass - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - - # add molecular weight variable to each time point, using correct units - for t in m.fs.time: - m.fs.cv.properties_out[t].mw_comp = Var( - m.fs.cv.properties_out[t].config.parameters.component_list, - units=units("mass") / units("amount"), - ) - - # add material balances to control volume - m.fs.cv.add_material_balances( - balance_type=MaterialBalanceType.componentPhase, - has_rate_reactions=True, - has_equilibrium_reactions=True, - has_phase_equilibrium=True, - ) - - assert_units_equivalent(m.fs.cv.rate_reaction_generation, rp_units) - assert_units_equivalent(m.fs.cv.rate_reaction_extent, rp_units) - assert_units_equivalent(m.fs.cv.equilibrium_reaction_generation, rp_units) - assert_units_equivalent(m.fs.cv.equilibrium_reaction_extent, rp_units) - assert_units_equivalent(m.fs.cv.inherent_reaction_generation, pp_units) - assert_units_equivalent(m.fs.cv.inherent_reaction_extent, pp_units) - assert_units_equivalent(m.fs.cv.phase_equilibrium_generation, pp_units) - - -@pytest.mark.unit -def test_add_material_balances_single_phase_w_equilibrium(caplog): - from idaes.models.properties import iapws95 - - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = iapws95.Iapws95ParameterBlock( - phase_presentation=iapws95.PhaseType.MIX, state_vars=iapws95.StateVars.PH - ) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - m.fs.cv.add_material_balances( - balance_type=MaterialBalanceType.useDefault, - has_phase_equilibrium=True, - ) - msg = ( - "DEPRECATED: Property package has only one phase; control volume cannot " - "include phase equilibrium terms. Some property packages support phase " - "equilibrium implicitly in which case additional terms are not " - "necessary. You should set has_phase_equilibrium=False. (deprecated in " - "2.0.0, will be removed in (or after) 3.0.0)" - ) - assert msg.replace(" ", "") in caplog.records[0].message.replace("\n", "").replace( - " ", "" - ) - - -# ----------------------------------------------------------------------------- -# Test add_phase_component_balances -@pytest.mark.unit -def test_add_phase_component_balances_default(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_phase_component_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_dynamic(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True - ) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_phase_component_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 8 - assert isinstance(m.fs.cv.phase_fraction, Var) - assert isinstance(m.fs.cv.material_holdup, Var) - assert isinstance(m.fs.cv.material_accumulation, Var) - - assert_units_consistent(m) - assert_units_equivalent(m.fs.cv.material_holdup, units.mol) - assert_units_equivalent(m.fs.cv.material_accumulation, units.mol / units.s) - - -@pytest.mark.unit -def test_add_phase_component_balances_dynamic_no_geometry(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True - ) - - # Do not add geometry - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_phase_component_balances() - - -@pytest.mark.unit -def test_add_phase_component_balances_rate_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_phase_component_balances(has_rate_reactions=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - assert isinstance(m.fs.cv.rate_reaction_generation, Var) - assert isinstance(m.fs.cv.rate_reaction_extent, Var) - assert isinstance(m.fs.cv.rate_reaction_stoichiometry_constraint, Constraint) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_rate_rxns_no_ReactionBlock(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_phase_component_balances(has_rate_reactions=True) - - -@pytest.mark.unit -def test_add_phase_component_balances_rate_rxns_no_rxn_idx(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.rp.del_component(m.fs.rp.rate_reaction_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_phase_component_balances(has_rate_reactions=True) - - -@pytest.mark.unit -def test_add_phase_component_balances_eq_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - - mb = m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - assert isinstance(m.fs.cv.equilibrium_reaction_generation, Var) - assert isinstance(m.fs.cv.equilibrium_reaction_extent, Var) - assert isinstance(m.fs.cv.equilibrium_reaction_stoichiometry_constraint, Constraint) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_eq_rxns_not_active(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) - - -@pytest.mark.unit -def test_add_phase_component_balances_eq_rxns_no_idx(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.rp.del_component(m.fs.rp.equilibrium_reaction_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) - - -@pytest.mark.unit -def test_add_phase_component_balances_in_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - # Set property package to contain inherent reactions - m.fs.pp._has_inherent_reactions = True - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - mb = m.fs.cv.add_phase_component_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - assert isinstance(m.fs.cv.inherent_reaction_generation, Var) - assert isinstance(m.fs.cv.inherent_reaction_extent, Var) - assert isinstance(m.fs.cv.inherent_reaction_stoichiometry_constraint, Constraint) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_in_rxns_no_idx(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - # Set property package to contain inherent reactions - m.fs.pp._has_inherent_reactions = True - # delete inherent_Reaction_dix to trigger exception - m.fs.pp.del_component(m.fs.pp.inherent_reaction_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + m.fs.cv = ExtendedControlVolume0DBlock(property_package=m.fs.pp) with pytest.raises( - PropertyNotSupportedError, - match=r"fs.cv Property package does not contain a " - r"list of inherent reactions \(inherent_reaction_idx\), " - r"but include_inherent_reactions is True.", + ConfigurationError, + match="fs.cv: isothermal energy balance option does not support custom terms." ): - m.fs.cv.add_phase_component_balances() - - -@pytest.mark.unit -def test_add_phase_component_balances_eq_rxns_no_ReactionBlock(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) - - -@pytest.mark.unit -def test_add_phase_component_balances_phase_eq(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_phase_component_balances(has_phase_equilibrium=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - assert isinstance(m.fs.cv.phase_equilibrium_generation, Var) - assert isinstance(m.fs.cv.config.property_package.phase_equilibrium_idx, Set) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_phase_eq_not_active(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_phase_component_balances(has_phase_equilibrium=True) - - -@pytest.mark.unit -def test_add_phase_component_balances_phase_eq_no_idx(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_phase_component_balances(has_phase_equilibrium=True) - - -@pytest.mark.unit -def test_add_phase_component_balances_mass_transfer(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_phase_component_balances(has_mass_transfer=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - assert isinstance(m.fs.cv.mass_transfer_term, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_custom_molar_term(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, - m.fs.cv.config.property_package.phase_list, - m.fs.cv.config.property_package.component_list, - ) - - def custom_method(t, p, j): - return m.fs.cv.test_var[t, p, j] * units.mol / units.s - - mb = m.fs.cv.add_phase_component_balances(custom_molar_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_custom_molar_term_no_mw(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 2 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, - m.fs.cv.config.property_package.phase_list, - m.fs.cv.config.property_package.component_list, - ) - - def custom_method(t, p, j): - return m.fs.cv.test_var[t, p, j] - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_phase_component_balances(custom_molar_term=custom_method) - - -@pytest.mark.unit -def test_add_phase_component_balances_custom_molar_term_mass_flow_basis(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 2 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, - m.fs.cv.config.property_package.phase_list, - m.fs.cv.config.property_package.component_list, - ) - - def custom_method(t, p, j): - return m.fs.cv.test_var[t, p, j] * units.mol / units.s - - for t in m.fs.time: - m.fs.cv.properties_out[t].mw_comp = Var( - m.fs.cv.properties_out[t].config.parameters.component_list, - units=units.kg / units.mol, - ) - - mb = m.fs.cv.add_phase_component_balances(custom_molar_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_custom_molar_term_undefined_basis(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 3 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, - m.fs.cv.config.property_package.phase_list, - m.fs.cv.config.property_package.component_list, - ) - - def custom_method(t, p, j): - return m.fs.cv.test_var[t, p, j] - - with pytest.raises(ConfigurationError): - m.fs.cv.add_phase_component_balances(custom_molar_term=custom_method) - - -@pytest.mark.unit -def test_add_phase_component_balances_custom_mass_term(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 2 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, - m.fs.cv.config.property_package.phase_list, - m.fs.cv.config.property_package.component_list, - ) - - def custom_method(t, p, j): - return m.fs.cv.test_var[t, p, j] * units.kg / units.s - - mb = m.fs.cv.add_phase_component_balances(custom_mass_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_custom_mass_term_no_mw(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 1 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, - m.fs.cv.config.property_package.phase_list, - m.fs.cv.config.property_package.component_list, - ) - - def custom_method(t, p, j): - return m.fs.cv.test_var[t, p, j] - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_phase_component_balances(custom_mass_term=custom_method) - - -@pytest.mark.unit -def test_add_phase_component_balances_custom_mass_term_mole_flow_basis(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 2 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, - m.fs.cv.config.property_package.phase_list, - m.fs.cv.config.property_package.component_list, - ) - - def custom_method(t, p, j): - return m.fs.cv.test_var[t, p, j] * units.kg / units.s - - for t in m.fs.time: - m.fs.cv.properties_out[t].mw_comp = Var( - m.fs.cv.properties_out[t].config.parameters.component_list, - units=units.kg / units.mol, - ) - - mb = m.fs.cv.add_phase_component_balances(custom_mass_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_phase_component_balances_custom_mass_term_undefined_basis(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 3 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, - m.fs.cv.config.property_package.phase_list, - m.fs.cv.config.property_package.component_list, - ) - - def custom_method(t, p, j): - return m.fs.cv.test_var[t, p, j] - - with pytest.raises(ConfigurationError): - m.fs.cv.add_phase_component_balances(custom_mass_term=custom_method) - - -# ----------------------------------------------------------------------------- -# Test add_total_component_balances -@pytest.mark.unit -def test_add_total_component_balances_default(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_component_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_dynamic(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True - ) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_component_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 4 - assert isinstance(m.fs.cv.phase_fraction, Var) - assert isinstance(m.fs.cv.material_holdup, Var) - assert isinstance(m.fs.cv.material_accumulation, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_dynamic_no_geometry(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True - ) - - # Do not add geometry - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_component_balances() - - -@pytest.mark.unit -def test_add_total_component_balances_rate_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_component_balances(has_rate_reactions=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - assert isinstance(m.fs.cv.rate_reaction_generation, Var) - assert isinstance(m.fs.cv.rate_reaction_extent, Var) - assert isinstance(m.fs.cv.rate_reaction_stoichiometry_constraint, Constraint) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_rate_rxns_no_ReactionBlock(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_component_balances(has_rate_reactions=True) - - -@pytest.mark.unit -def test_add_total_component_balances_rate_rxns_no_rxn_idx(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.rp.del_component(m.fs.rp.rate_reaction_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_total_component_balances(has_rate_reactions=True) - - -@pytest.mark.unit -def test_add_total_component_balances_eq_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - - mb = m.fs.cv.add_total_component_balances(has_equilibrium_reactions=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - assert isinstance(m.fs.cv.equilibrium_reaction_generation, Var) - assert isinstance(m.fs.cv.equilibrium_reaction_extent, Var) - assert isinstance(m.fs.cv.equilibrium_reaction_stoichiometry_constraint, Constraint) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_eq_rxns_not_active(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_component_balances(has_equilibrium_reactions=True) - - -@pytest.mark.unit -def test_add_total_component_balances_eq_rxns_no_idx(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.rp.del_component(m.fs.rp.equilibrium_reaction_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_total_component_balances(has_equilibrium_reactions=True) - - -@pytest.mark.unit -def test_add_total_component_balances_eq_rxns_no_ReactionBlock(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_component_balances(has_equilibrium_reactions=True) - - -@pytest.mark.unit -def test_add_total_component_balances_in_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - # Set property package to contain inherent reactions - m.fs.pp._has_inherent_reactions = True - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - mb = m.fs.cv.add_total_component_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - assert isinstance(m.fs.cv.inherent_reaction_generation, Var) - assert isinstance(m.fs.cv.inherent_reaction_extent, Var) - assert isinstance(m.fs.cv.inherent_reaction_stoichiometry_constraint, Constraint) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_in_rxns_no_idx(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - # Set property package to contain inherent reactions - m.fs.pp._has_inherent_reactions = True - # delete inherent_Reaction_dix to trigger exception - m.fs.pp.del_component(m.fs.pp.inherent_reaction_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - with pytest.raises( - PropertyNotSupportedError, - match=r"fs.cv Property package does not contain a " - r"list of inherent reactions \(inherent_reaction_idx\), " - r"but include_inherent_reactions is True.", - ): - m.fs.cv.add_total_component_balances() - - -@pytest.mark.unit -def test_add_total_component_balances_phase_eq_not_active(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_component_balances(has_phase_equilibrium=True) - - -@pytest.mark.unit -def test_add_total_component_balances_mass_transfer(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_component_balances(has_mass_transfer=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - assert isinstance(m.fs.cv.mass_transfer_term, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_custom_molar_term(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list - ) - - def custom_method(t, j): - return m.fs.cv.test_var[t, j] * units.mol / units.s - - mb = m.fs.cv.add_total_component_balances(custom_molar_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_custom_molar_term_no_mw(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 2 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list - ) - - def custom_method(t, j): - return m.fs.cv.test_var[t, j] - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_total_component_balances(custom_molar_term=custom_method) - - -@pytest.mark.unit -def test_add_total_component_balances_custom_molar_term_mass_flow_basis(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 2 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list - ) - - def custom_method(t, j): - return m.fs.cv.test_var[t, j] * units.mol / units.s - - for t in m.fs.time: - m.fs.cv.properties_out[t].mw_comp = Var( - m.fs.cv.properties_out[t].config.parameters.component_list, - units=units.kg / units.mol, - ) - - mb = m.fs.cv.add_total_component_balances(custom_molar_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_custom_molar_term_undefined_basis(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 3 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list - ) - - def custom_method(t, j): - return m.fs.cv.test_var[t, j] - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_component_balances(custom_molar_term=custom_method) - - -@pytest.mark.unit -def test_add_total_component_balances_custom_mass_term(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 2 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list - ) - - def custom_method(t, j): - return m.fs.cv.test_var[t, j] * units.kg / units.s - - mb = m.fs.cv.add_total_component_balances(custom_mass_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_custom_mass_term_no_mw(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 1 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list - ) - - def custom_method(t, j): - return m.fs.cv.test_var[t, j] - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_total_component_balances(custom_mass_term=custom_method) - - -@pytest.mark.unit -def test_add_total_component_balances_custom_mass_term_mole_flow_basis(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 2 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list - ) - - def custom_method(t, j): - return m.fs.cv.test_var[t, j] * units.kg / units.s - - for t in m.fs.time: - m.fs.cv.properties_out[t].mw_comp = Var( - m.fs.cv.properties_out[t].config.parameters.component_list, - units=units.kg / units.mol, - ) - - mb = m.fs.cv.add_total_component_balances(custom_mass_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_component_balances_custom_mass_term_undefined_basis(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.pp.basis_switch = 3 - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var( - m.fs.cv.flowsheet().time, m.fs.cv.config.property_package.component_list - ) - - def custom_method(t, j): - return m.fs.cv.test_var[t, j] - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_component_balances(custom_mass_term=custom_method) - - -# ----------------------------------------------------------------------------- -# Test add_total_element_balances -@pytest.mark.unit -def test_add_total_element_balances_default(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_element_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 3 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_element_balances_properties_not_supported(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.element_list) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(PropertyNotSupportedError): - m.fs.cv.add_total_element_balances() - - -@pytest.mark.unit -def test_add_total_element_balances_dynamic(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True - ) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_element_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 6 - assert isinstance(m.fs.cv.phase_fraction, Var) - assert isinstance(m.fs.cv.element_holdup, Var) - assert isinstance(m.fs.cv.element_accumulation, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_element_balances_dynamic_no_geometry(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True - ) - - # Do not add geometry - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_element_balances() - - -@pytest.mark.unit -def test_add_total_element_balances_rate_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_element_balances(has_rate_reactions=True) - - -@pytest.mark.unit -def test_add_total_element_balances_eq_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_element_balances(has_equilibrium_reactions=True) - - -@pytest.mark.unit -def test_add_total_element_balances_phase_eq(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_element_balances(has_phase_equilibrium=True) - - -@pytest.mark.unit -def test_add_total_element_balances_mass_transfer(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_element_balances(has_mass_transfer=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 3 - assert isinstance(m.fs.cv.elemental_mass_transfer_term, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_element_balances_custom_term(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var(m.fs.cv.flowsheet().time, m.fs.pp.element_list) - - def custom_method(t, e): - return m.fs.cv.test_var[t, e] * units.mol / units.s - - mb = m.fs.cv.add_total_element_balances(custom_elemental_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 3 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_element_balances_lineraly_dependent(caplog): - caplog.set_level(idaeslog.INFO_LOW) - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - - # Change elemental composition to introduce dependency - m.fs.pp.element_comp = { - "c1": {"H": 0, "He": 0, "Li": 1}, - "c2": {"H": 1, "He": 2, "Li": 0}, - } - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - - mb = m.fs.cv.add_total_element_balances() - # Check that logger message was recorded and has the right level - msg = ( - "fs.cv detected linearly dependent element balance equations. " - "Element balances will NOT be written for the following elements: " - "['He']" - ) - assert msg in caplog.text - for record in caplog.records: - if "['He']" in record.msg: - assert record.levelno == idaeslog.INFO_LOW - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - for e in mb: - # H and Li are not lineraly dependent and should have constraints - assert e in [(0, "H"), (0, "Li")] - # He is lineraly dependent on H and should be skipped - - assert_units_consistent(m) - - -# ----------------------------------------------------------------------------- -# Test unsupported material balance types -@pytest.mark.unit -def test_add_total_material_balances(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(BalanceTypeNotSupportedError): - m.fs.cv.add_total_material_balances() - - -# ----------------------------------------------------------------------------- -# Test add_energy_balances default -@pytest.mark.unit -def test_add_energy_balances_default_fail(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.pp.default_balance_switch = 2 - - with pytest.raises(ConfigurationError): - m.fs.cv.add_energy_balances(EnergyBalanceType.useDefault) - - -@pytest.mark.unit -def test_add_energy_balances_default(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - eb = m.fs.cv.add_energy_balances(EnergyBalanceType.useDefault) - - assert isinstance(eb, Constraint) - assert len(eb) == 1 - - assert_units_consistent(m) - - -# ----------------------------------------------------------------------------- -# Test phase enthalpy balances -@pytest.mark.unit -def test_add_total_enthalpy_balances_default(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - eb = m.fs.cv.add_total_enthalpy_balances() - - assert isinstance(eb, Constraint) - assert len(eb) == 1 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_enthalpy_balances_dynamic(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True - ) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_enthalpy_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 2 - assert isinstance(m.fs.cv.phase_fraction, Var) - assert isinstance(m.fs.cv.energy_holdup, Var) - assert isinstance(m.fs.cv.energy_accumulation, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_enthalpy_balances_dynamic_no_geometry(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, reaction_package=m.fs.rp, dynamic=True - ) - - # Do not add geometry - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_enthalpy_balances() - - -@pytest.mark.unit -def test_add_total_enthalpy_balances_heat_transfer(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_enthalpy_balances(has_heat_transfer=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 1 - assert isinstance(m.fs.cv.heat, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_enthalpy_balances_work_transfer(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_enthalpy_balances(has_work_transfer=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 1 - assert isinstance(m.fs.cv.work, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_enthalpy_balances_enthalpy_transfer(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_enthalpy_balances(has_enthalpy_transfer=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 1 - assert isinstance(m.fs.cv.enthalpy_transfer, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_enthalpy_balances_custom_term(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var(m.fs.cv.flowsheet().time) - - def custom_method(t): - return m.fs.cv.test_var[t] * units.J / units.s - - mb = m.fs.cv.add_total_enthalpy_balances(custom_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 1 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_enthalpy_balances_dh_rxn_no_extents(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(ConfigurationError): - m.fs.cv.add_total_enthalpy_balances(has_heat_of_reaction=True) - - -@pytest.mark.unit -def test_add_total_enthalpy_balances_dh_rxn_rate_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - m.fs.cv.add_phase_component_balances(has_rate_reactions=True) - - m.fs.cv.add_total_enthalpy_balances(has_heat_of_reaction=True) - assert isinstance(m.fs.cv.heat_of_reaction, Expression) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_enthalpy_balances_dh_rxn_equil_rxns(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - m.fs.cv.add_phase_component_balances(has_equilibrium_reactions=True) - - m.fs.cv.add_total_enthalpy_balances(has_heat_of_reaction=True) - assert isinstance(m.fs.cv.heat_of_reaction, Expression) - - assert_units_consistent(m) - - -# ----------------------------------------------------------------------------- -# Test unsupported energy balance types -@pytest.mark.unit -def test_add_phase_enthalpy_balances(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(BalanceTypeNotSupportedError): - m.fs.cv.add_phase_enthalpy_balances() - - -@pytest.mark.unit -def test_add_phase_energy_balances(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(BalanceTypeNotSupportedError): - m.fs.cv.add_phase_energy_balances() - - -@pytest.mark.unit -def test_add_total_energy_balances(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(BalanceTypeNotSupportedError): - m.fs.cv.add_total_energy_balances() - - -@pytest.mark.unit -def test_add_isothermal_energy_balances(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(BalanceTypeNotSupportedError): - m.fs.cv.add_isothermal_constraint() - - -# ----------------------------------------------------------------------------- -# Test add total pressure balances -@pytest.mark.unit -def test_add_total_pressure_balances_default(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - eb = m.fs.cv.add_total_pressure_balances() - - assert isinstance(eb, Constraint) - assert len(eb) == 1 - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_pressure_balances_deltaP(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_total_pressure_balances(has_pressure_change=True) - - assert isinstance(mb, Constraint) - assert len(mb) == 1 - assert isinstance(m.fs.cv.deltaP, Var) - - assert_units_consistent(m) - - -@pytest.mark.unit -def test_add_total_pressure_balances_custom_term(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.test_var = Var(m.fs.cv.flowsheet().time) - - def custom_method(t): - return m.fs.cv.test_var[t] * units.Pa - - mb = m.fs.cv.add_total_pressure_balances(custom_term=custom_method) - - assert isinstance(mb, Constraint) - assert len(mb) == 1 - - assert_units_consistent(m) - - -# ----------------------------------------------------------------------------- -# Test unsupported momentum balance types -@pytest.mark.unit -def test_add_phase_pressure_balances(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(BalanceTypeNotSupportedError): - m.fs.cv.add_phase_pressure_balances() - - -@pytest.mark.unit -def test_add_phase_momentum_balances(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(BalanceTypeNotSupportedError): - m.fs.cv.add_phase_momentum_balances() - - -@pytest.mark.unit -def test_add_total_momentum_balances(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - with pytest.raises(BalanceTypeNotSupportedError): - m.fs.cv.add_total_momentum_balances() - - -# ----------------------------------------------------------------------------- -# Test model checks, initialize and release_state -@pytest.mark.unit -def test_model_checks(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - m.fs.cv.model_check() - - for t in m.fs.time: - assert m.fs.cv.properties_in[t].check is True - assert m.fs.cv.properties_out[t].check is True - assert m.fs.cv.reactions[t].check is True - - -@pytest.mark.unit -def test_initialize(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - f = m.fs.cv.initialize() - - for t in m.fs.time: - assert m.fs.cv.properties_in[t].init_test is True - assert m.fs.cv.properties_out[t].init_test is True - assert m.fs.cv.properties_in[t].hold_state is True - assert m.fs.cv.properties_out[t].hold_state is False - assert m.fs.cv.reactions[t].init_test is True - - m.fs.cv.release_state(flags=f) - - for t in m.fs.time: - assert m.fs.cv.properties_in[t].hold_state is False - assert m.fs.cv.properties_out[t].hold_state is False - - -@pytest.mark.unit -def test_get_stream_table_contents(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - - df = m.fs.cv._get_stream_table_contents() - - assert df.loc["component_flow_phase ('p1', 'c1')"]["In"] == 2 - assert df.loc["component_flow_phase ('p1', 'c2')"]["In"] == 2 - assert df.loc["component_flow_phase ('p2', 'c1')"]["In"] == 2 - assert df.loc["component_flow_phase ('p2', 'c2')"]["In"] == 2 - assert df.loc["pressure"]["In"] == 1e5 - assert df.loc["temperature"]["In"] == 300 - - assert df.loc["component_flow_phase ('p1', 'c1')"]["Out"] == 2 - assert df.loc["component_flow_phase ('p1', 'c2')"]["Out"] == 2 - assert df.loc["component_flow_phase ('p2', 'c1')"]["Out"] == 2 - assert df.loc["component_flow_phase ('p2', 'c2')"]["Out"] == 2 - assert df.loc["pressure"]["Out"] == 1e5 - assert df.loc["temperature"]["Out"] == 300 - - -@pytest.mark.unit -def test_get_performance_contents(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - m.fs.cv.add_material_balances( - has_rate_reactions=True, - has_equilibrium_reactions=True, - has_phase_equilibrium=True, - has_mass_transfer=True, - ) - m.fs.cv.add_energy_balances( - has_heat_of_reaction=True, has_work_transfer=True, has_heat_transfer=True - ) - m.fs.cv.add_momentum_balances(has_pressure_change=True) - - dd = m.fs.cv._get_performance_contents() - - assert len(dd) == 3 - for k in dd.keys(): - assert k in ("vars", "exprs", "params") - assert len(dd["vars"]) == 36 - for k in dd["vars"].keys(): - assert k in [ - "Volume", - "Heat Transfer", - "Work Transfer", - "Pressure Change", - "Phase Fraction [p1]", - "Phase Fraction [p2]", - "Energy Holdup [p1]", - "Energy Holdup [p2]", - "Energy Accumulation [p1]", - "Energy Accumulation [p2]", - "Material Holdup [p1, c1]", - "Material Holdup [p1, c2]", - "Material Holdup [p2, c1]", - "Material Holdup [p2, c2]", - "Material Accumulation [p1, c1]", - "Material Accumulation [p1, c2]", - "Material Accumulation [p2, c1]", - "Material Accumulation [p2, c2]", - "Rate Reaction Generation [p1, c1]", - "Rate Reaction Generation [p1, c2]", - "Rate Reaction Generation [p2, c1]", - "Rate Reaction Generation [p2, c2]", - "Equilibrium Reaction Generation [p1, c1]", - "Equilibrium Reaction Generation [p1, c2]", - "Equilibrium Reaction Generation [p2, c1]", - "Equilibrium Reaction Generation [p2, c2]", - "Mass Transfer Term [p1, c1]", - "Mass Transfer Term [p1, c2]", - "Mass Transfer Term [p2, c1]", - "Mass Transfer Term [p2, c2]", - "Rate Reaction Extent [r1]", - "Rate Reaction Extent [r2]", - "Equilibrium Reaction Extent [e1]", - "Equilibrium Reaction Extent [e2]", - "Phase Equilibrium Generation [e1]", - "Phase Equilibrium Generation [e2]", - ] - - assert len(dd["exprs"]) == 1 - for k in dd["exprs"].keys(): - assert k in ["Heat of Reaction Term"] - - assert len(dd["params"]) == 0 - - -@pytest.mark.unit -def test_get_performance_contents_elemental(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - m.fs.cv.add_total_element_balances(has_mass_transfer=True) - m.fs.cv.add_energy_balances( - has_heat_of_reaction=False, has_work_transfer=True, has_heat_transfer=True - ) - m.fs.cv.add_momentum_balances(has_pressure_change=True) - - dd = m.fs.cv._get_performance_contents() - - assert len(dd) == 3 - for k in dd.keys(): - assert k in ("vars", "exprs", "params") - assert len(dd["vars"]) == 19 - for k in dd["vars"].keys(): - assert k in [ - "Volume", - "Heat Transfer", - "Work Transfer", - "Pressure Change", - "Phase Fraction [p1]", - "Phase Fraction [p2]", - "Energy Holdup [p1]", - "Energy Holdup [p2]", - "Energy Accumulation [p1]", - "Energy Accumulation [p2]", - "Elemental Holdup [H]", - "Elemental Holdup [He]", - "Elemental Holdup [Li]", - "Elemental Accumulation [H]", - "Elemental Accumulation [He]", - "Elemental Accumulation [Li]", - "Elemental Transfer Term [H]", - "Elemental Transfer Term [He]", - "Elemental Transfer Term [Li]", - ] - - assert len(dd["exprs"]) == 12 - for k in dd["exprs"].keys(): - assert k in [ - "Element Flow In [p1, H]", - "Element Flow In [p1, He]", - "Element Flow In [p1, Li]", - "Element Flow In [p2, H]", - "Element Flow In [p2, He]", - "Element Flow In [p2, Li]", - "Element Flow Out [p1, H]", - "Element Flow Out [p1, He]", - "Element Flow Out [p1, Li]", - "Element Flow Out [p2, H]", - "Element Flow Out [p2, He]", - "Element Flow Out [p2, Li]", - ] - - assert len(dd["params"]) == 0 - - -@pytest.mark.unit -def test_reports(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=False) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=True) - m.fs.cv.add_material_balances( - has_rate_reactions=True, - has_equilibrium_reactions=True, - has_phase_equilibrium=True, - ) - m.fs.cv.add_energy_balances(has_heat_of_reaction=True, has_heat_transfer=True) - m.fs.cv.add_momentum_balances(has_pressure_change=True) - - m.fs.cv.report() - - -@pytest.mark.unit -def test_dynamic_mass_basis(): - m = ConcreteModel() - m.fs = Flowsheet(dynamic=True, time_units=units.s) - m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.basis_switch = 2 - m.fs.cv = ControlVolume0DBlock( - property_package=m.fs.pp, - reaction_package=m.fs.rp, - dynamic=True, - ) - - m.fs.cv.add_geometry() - m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) - - mb = m.fs.cv.add_phase_component_balances() - - assert isinstance(mb, Constraint) - assert len(mb) == 8 - assert isinstance(m.fs.cv.phase_fraction, Var) - assert isinstance(m.fs.cv.material_holdup, Var) - assert isinstance(m.fs.cv.material_accumulation, Var) - - assert_units_consistent(m) - assert_units_equivalent(m.fs.cv.material_holdup, units.kg) - assert_units_equivalent(m.fs.cv.material_accumulation, units.kg / units.s) + m.fs.cv.add_isothermal_constraint(custom_term="foo") diff --git a/idaes/core/base/tests/test_extended_control_volume_1d.py b/idaes/core/base/tests/test_extended_control_volume_1d.py new file mode 100644 index 0000000000..a1d51d1a1c --- /dev/null +++ b/idaes/core/base/tests/test_extended_control_volume_1d.py @@ -0,0 +1,203 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2024 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# +""" +Tests for ExtendedControlVolumeBlockData. + +Author: Andrew Lee +""" +import pytest +from pyomo.environ import ConcreteModel, Constraint, units +from pyomo.util.check_units import assert_units_consistent + +from idaes.core import ( + ExtendedControlVolume1DBlock, + FlowsheetBlockData, + declare_process_block_class, +) +from idaes.core.util.exceptions import ( + ConfigurationError, +) +from idaes.core.util.testing import ( + PhysicalParameterTestBlock, +) + + +# ----------------------------------------------------------------------------- +# Mockup classes for testing +@declare_process_block_class("Flowsheet") +class _Flowsheet(FlowsheetBlockData): + def build(self): + super(_Flowsheet, self).build() + + +@pytest.mark.unit +def test_add_isothermal_constraint(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ExtendedControlVolume1DBlock( + property_package=m.fs.pp, + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + finite_elements=10, + ) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + m.fs.cv.add_isothermal_constraint() + + assert isinstance(m.fs.cv.enthalpy_balances, Constraint) + assert len(m.fs.cv.enthalpy_balances) == 1 # x==0 is skipped + assert str(m.fs.cv.enthalpy_balances[0, 1].expr) == str( + m.fs.cv.properties[0, 0].temperature == m.fs.cv.properties[0, 1].temperature + ) + assert (0, 0) not in m.fs.cv.enthalpy_balances + + assert_units_consistent(m.fs.cv) + + +@pytest.mark.unit +def test_add_isothermal_constraint_dynamic(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=True, time_set=[0, 1, 2, 3], time_units=units.s) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ExtendedControlVolume1DBlock( + property_package=m.fs.pp, + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + finite_elements=10, + ) + + m.fs.cv.add_geometry() + m.fs.cv.add_state_blocks(has_phase_equilibrium=False) + + m.fs.cv.add_isothermal_constraint() + + assert isinstance(m.fs.cv.enthalpy_balances, Constraint) + assert len(m.fs.cv.enthalpy_balances) == 4 # x==0 is skipped + for t in m.fs.time: + assert str(m.fs.cv.enthalpy_balances[t, 1].expr) == str( + m.fs.cv.properties[t, 0].temperature == m.fs.cv.properties[t, 1].temperature + ) + assert (t, 0) not in m.fs.cv.enthalpy_balances + + assert_units_consistent(m.fs.cv) + + +@pytest.mark.unit +def test_add_isothermal_constraint_heat_transfer(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ExtendedControlVolume1DBlock( + property_package=m.fs.pp, + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + finite_elements=10, + ) + + with pytest.raises( + ConfigurationError, + match="fs.cv: isothermal energy balance option requires that has_heat_transfer is False. " + "If you are trying to solve for heat duty to achieve isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ): + m.fs.cv.add_isothermal_constraint(has_heat_transfer=True) + + +@pytest.mark.unit +def test_add_isothermal_constraint_work_transfer(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ExtendedControlVolume1DBlock( + property_package=m.fs.pp, + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + finite_elements=10, + ) + + with pytest.raises( + ConfigurationError, + match="fs.cv: isothermal energy balance option requires that has_work_transfer is False. " + "If you are trying to solve for work under isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ): + m.fs.cv.add_isothermal_constraint(has_work_transfer=True) + + +@pytest.mark.unit +def test_add_isothermal_constraint_enthalpy_transfer(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ExtendedControlVolume1DBlock( + property_package=m.fs.pp, + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + finite_elements=10, + ) + + with pytest.raises( + ConfigurationError, + match="fs.cv: isothermal energy balance option does not support enthalpy transfer." + ): + m.fs.cv.add_isothermal_constraint(has_enthalpy_transfer=True) + + +@pytest.mark.unit +def test_add_isothermal_constraint_heat_of_rxn(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ExtendedControlVolume1DBlock( + property_package=m.fs.pp, + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + finite_elements=10, + ) + + with pytest.raises( + ConfigurationError, + match="fs.cv: isothermal energy balance option requires that has_heat_of_reaction is False. " + "If you are trying to solve for heat duty to achieve isothermal operation, please use " + "a full energy balance as add a constraint to equate inlet and outlet temperatures." + ): + m.fs.cv.add_isothermal_constraint(has_heat_of_reaction=True) + + +@pytest.mark.unit +def test_add_isothermal_constraint_custom_term(): + m = ConcreteModel() + m.fs = Flowsheet(dynamic=False) + m.fs.pp = PhysicalParameterTestBlock() + + m.fs.cv = ExtendedControlVolume1DBlock( + property_package=m.fs.pp, + transformation_method="dae.finite_difference", + transformation_scheme="BACKWARD", + finite_elements=10, + ) + + with pytest.raises( + ConfigurationError, + match="fs.cv: isothermal energy balance option does not support custom terms." + ): + m.fs.cv.add_isothermal_constraint(custom_term="foo") From eaeabc4c42e964509bb6b66f1b06769dc041778b Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 15 Jan 2025 11:25:51 -0500 Subject: [PATCH 03/14] Docs for extended CVs --- .../core/control_volume_0d.rst | 28 +++++++++++++++++ .../core/control_volume_1d.rst | 30 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/docs/reference_guides/core/control_volume_0d.rst b/docs/reference_guides/core/control_volume_0d.rst index da9dad07f7..27bb20dd58 100644 --- a/docs/reference_guides/core/control_volume_0d.rst +++ b/docs/reference_guides/core/control_volume_0d.rst @@ -282,3 +282,31 @@ A single pressure balance is written for the entire mixture. .. math:: 0 = s_{pressure} \times P_{in, t} - s_{pressure} \times P_{out, t} + s_{pressure} \times \Delta P_t + s_{pressure} \times \Delta P_{custom, t} The :math:`\Delta P_{custom, t}` term allows the user to provide custom terms which will be added into the pressure balance. + + +Extended 0D Control Volume Class +-------------------------------- + +The ExtendedControlVolume0DBlock block builds upon ControlVolume0DBlock by adding some new balance options. It is envisioned that this will +merge with ControlVolume0DBlock, however to ensure backward compatibility these additions have been kept separate until unit models can +be updated to restrict (or allow) these new options if necessary. The core functionality is the same as for ControlVolume0DBlock, with the +addition of one extra energy balance type; isothermal. + +.. module:: idaes.core.base.extended_control_volume0d + +.. autoclass:: ExtendedControlVolume0DBlock + :members: + +.. autoclass:: ExtendedControlVolume0DBlockData + :members: + +add_isothermal_constraint +^^^^^^^^^^^^^^^^^^^^^^^^^ + +A constraint equating temperature at the inlet and outlet of the control volume is written. + +**Constraints** + +`enthalpy_balances(t)`: + +.. math:: P_{in, t} == P_{out, t} diff --git a/docs/reference_guides/core/control_volume_1d.rst b/docs/reference_guides/core/control_volume_1d.rst index da7aa57bb5..90aca1645b 100644 --- a/docs/reference_guides/core/control_volume_1d.rst +++ b/docs/reference_guides/core/control_volume_1d.rst @@ -311,3 +311,33 @@ The :math:`\Delta P_{custom, t, x}` term allows the user to provide custom terms `pressure_linking_constraint(t, x)`: This constraint is an internal constraint used to link the pressure terms in the StateBlocks into a single indexed variable. This is required as Pyomo.DAE requires a single indexed variable to create the associated DerivativeVars and their numerical expansions. + + +Extended 1D Control Volume Class +-------------------------------- + +The ExtendedControlVolume1DBlock block builds upon ControlVolume1DBlock by adding some new balance options. It is envisioned that this will +merge with ControlVolume1DBlock, however to ensure backward compatibility these additions have been kept separate until unit models can +be updated to restrict (or allow) these new options if necessary. The core functionality is the same as for ControlVolume1DBlock, with the +addition of one extra energy balance type; isothermal. + +.. module:: idaes.core.base.extended_control_volume1d + +.. autoclass:: ExtendedControlVolume1DBlock + :members: + +.. autoclass:: ExtendedControlVolume1DBlockData + :members: + +add_isothermal_constraint +^^^^^^^^^^^^^^^^^^^^^^^^^ + +A constraint equating temperature along the length domain of the control volume is written. + +**Constraints** + +`enthalpy_balances(t)`: + +.. math:: P_{t, x-1} == P_{t, x} + +This constraint is skipped at the inlet to the control volume. From 70c347503672dcb766cc0b72177b5e82225252fd Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 15 Jan 2025 11:41:06 -0500 Subject: [PATCH 04/14] Black and pylint --- idaes/core/base/extended_control_volume0d.py | 3 --- idaes/core/base/extended_control_volume1d.py | 13 +++++++++---- .../base/tests/test_extended_control_volume_0d.py | 13 +++++++------ .../base/tests/test_extended_control_volume_1d.py | 10 +++++----- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/idaes/core/base/extended_control_volume0d.py b/idaes/core/base/extended_control_volume0d.py index 7b711b6607..7c3f86b586 100644 --- a/idaes/core/base/extended_control_volume0d.py +++ b/idaes/core/base/extended_control_volume0d.py @@ -16,9 +16,6 @@ __author__ = "Andrew Lee" -# Import Pyomo libraries -from pyomo.environ import Constraint, Reals, units as pyunits, Var, value - # Import IDAES cores from idaes.core.base.control_volume0d import ControlVolume0DBlockData from idaes.core import declare_process_block_class diff --git a/idaes/core/base/extended_control_volume1d.py b/idaes/core/base/extended_control_volume1d.py index 6dee23eed3..6130abd6d9 100644 --- a/idaes/core/base/extended_control_volume1d.py +++ b/idaes/core/base/extended_control_volume1d.py @@ -17,7 +17,7 @@ __author__ = "Andrew Lee" # Import Pyomo libraries -from pyomo.environ import Constraint, Reals, units as pyunits, Var, value +from pyomo.environ import Constraint # Import IDAES cores from idaes.core.base.control_volume1d import ControlVolume1DBlockData @@ -104,7 +104,9 @@ def add_isothermal_constraint( ) # Add isothermal constraint - @self.Constraint(self.flowsheet().time, self.length_domain, doc="Energy balances") + @self.Constraint( + self.flowsheet().time, self.length_domain, doc="Energy balances" + ) def enthalpy_balances(b, t, x): if ( b.config.transformation_scheme != "FORWARD" @@ -114,5 +116,8 @@ def enthalpy_balances(b, t, x): and x == b.length_domain.last() ): return Constraint.Skip - else: - return b.properties[t, b.length_domain.prev(x)].temperature == b.properties[t, x].temperature + + return ( + b.properties[t, b.length_domain.prev(x)].temperature + == b.properties[t, x].temperature + ) diff --git a/idaes/core/base/tests/test_extended_control_volume_0d.py b/idaes/core/base/tests/test_extended_control_volume_0d.py index 73b8b82590..5c558cbb45 100644 --- a/idaes/core/base/tests/test_extended_control_volume_0d.py +++ b/idaes/core/base/tests/test_extended_control_volume_0d.py @@ -79,7 +79,8 @@ def test_add_isothermal_constraint_dynamic(): assert len(m.fs.cv.enthalpy_balances) == 4 for t in m.fs.time: assert str(m.fs.cv.enthalpy_balances[t].expr) == str( - m.fs.cv.properties_in[t].temperature == m.fs.cv.properties_out[t].temperature + m.fs.cv.properties_in[t].temperature + == m.fs.cv.properties_out[t].temperature ) assert_units_consistent(m.fs.cv) @@ -97,7 +98,7 @@ def test_add_isothermal_constraint_heat_transfer(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_heat_transfer is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance as add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_heat_transfer=True) @@ -114,7 +115,7 @@ def test_add_isothermal_constraint_work_transfer(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_work_transfer is False. " "If you are trying to solve for work under isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance as add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_work_transfer=True) @@ -129,7 +130,7 @@ def test_add_isothermal_constraint_enthalpy_transfer(): with pytest.raises( ConfigurationError, - match="fs.cv: isothermal energy balance option does not support enthalpy transfer." + match="fs.cv: isothermal energy balance option does not support enthalpy transfer.", ): m.fs.cv.add_isothermal_constraint(has_enthalpy_transfer=True) @@ -146,7 +147,7 @@ def test_add_isothermal_constraint_heat_of_rxn(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_heat_of_reaction is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance as add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_heat_of_reaction=True) @@ -161,6 +162,6 @@ def test_add_isothermal_constraint_custom_term(): with pytest.raises( ConfigurationError, - match="fs.cv: isothermal energy balance option does not support custom terms." + match="fs.cv: isothermal energy balance option does not support custom terms.", ): m.fs.cv.add_isothermal_constraint(custom_term="foo") diff --git a/idaes/core/base/tests/test_extended_control_volume_1d.py b/idaes/core/base/tests/test_extended_control_volume_1d.py index a1d51d1a1c..fa66873063 100644 --- a/idaes/core/base/tests/test_extended_control_volume_1d.py +++ b/idaes/core/base/tests/test_extended_control_volume_1d.py @@ -114,7 +114,7 @@ def test_add_isothermal_constraint_heat_transfer(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_heat_transfer is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance as add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_heat_transfer=True) @@ -136,7 +136,7 @@ def test_add_isothermal_constraint_work_transfer(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_work_transfer is False. " "If you are trying to solve for work under isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance as add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_work_transfer=True) @@ -156,7 +156,7 @@ def test_add_isothermal_constraint_enthalpy_transfer(): with pytest.raises( ConfigurationError, - match="fs.cv: isothermal energy balance option does not support enthalpy transfer." + match="fs.cv: isothermal energy balance option does not support enthalpy transfer.", ): m.fs.cv.add_isothermal_constraint(has_enthalpy_transfer=True) @@ -178,7 +178,7 @@ def test_add_isothermal_constraint_heat_of_rxn(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_heat_of_reaction is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance as add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_heat_of_reaction=True) @@ -198,6 +198,6 @@ def test_add_isothermal_constraint_custom_term(): with pytest.raises( ConfigurationError, - match="fs.cv: isothermal energy balance option does not support custom terms." + match="fs.cv: isothermal energy balance option does not support custom terms.", ): m.fs.cv.add_isothermal_constraint(custom_term="foo") From feee062bdf2c8fcbe901a88cb476549f6bc78e63 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 15 Jan 2025 11:59:57 -0500 Subject: [PATCH 05/14] Apply suggestions from code review Co-authored-by: Adam Atia --- docs/reference_guides/core/control_volume_0d.rst | 2 +- docs/reference_guides/core/control_volume_1d.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference_guides/core/control_volume_0d.rst b/docs/reference_guides/core/control_volume_0d.rst index 27bb20dd58..4149f80834 100644 --- a/docs/reference_guides/core/control_volume_0d.rst +++ b/docs/reference_guides/core/control_volume_0d.rst @@ -309,4 +309,4 @@ A constraint equating temperature at the inlet and outlet of the control volume `enthalpy_balances(t)`: -.. math:: P_{in, t} == P_{out, t} +.. math:: T_{in, t} == T_{out, t} diff --git a/docs/reference_guides/core/control_volume_1d.rst b/docs/reference_guides/core/control_volume_1d.rst index 90aca1645b..ba34b22834 100644 --- a/docs/reference_guides/core/control_volume_1d.rst +++ b/docs/reference_guides/core/control_volume_1d.rst @@ -338,6 +338,6 @@ A constraint equating temperature along the length domain of the control volume `enthalpy_balances(t)`: -.. math:: P_{t, x-1} == P_{t, x} +.. math:: T_{t, x-1} == T_{t, x} This constraint is skipped at the inlet to the control volume. From 94765655d9eda95b2c9a4b5a4f51d53484426afc Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 15 Jan 2025 12:12:30 -0500 Subject: [PATCH 06/14] Fix XCV1D isothermal contraint --- idaes/core/base/extended_control_volume1d.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/idaes/core/base/extended_control_volume1d.py b/idaes/core/base/extended_control_volume1d.py index 6130abd6d9..1fc4c90beb 100644 --- a/idaes/core/base/extended_control_volume1d.py +++ b/idaes/core/base/extended_control_volume1d.py @@ -108,13 +108,7 @@ def add_isothermal_constraint( self.flowsheet().time, self.length_domain, doc="Energy balances" ) def enthalpy_balances(b, t, x): - if ( - b.config.transformation_scheme != "FORWARD" - and x == b.length_domain.first() - ) or ( - b.config.transformation_scheme == "FORWARD" - and x == b.length_domain.last() - ): + if x == b.length_domain.first(): return Constraint.Skip return ( From 6b9ea949e47b2300ffe481f1fac55ea012aef593 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 15 Jan 2025 12:22:40 -0500 Subject: [PATCH 07/14] Cleaning up some cruft in tests --- idaes/core/base/tests/test_control_volume_0d.py | 6 +----- idaes/core/base/tests/test_control_volume_1d.py | 4 ---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/idaes/core/base/tests/test_control_volume_0d.py b/idaes/core/base/tests/test_control_volume_0d.py index 30df1674df..4b083430c8 100644 --- a/idaes/core/base/tests/test_control_volume_0d.py +++ b/idaes/core/base/tests/test_control_volume_0d.py @@ -2285,14 +2285,10 @@ def test_add_isothermal_energy_balances(): m = ConcreteModel() m.fs = Flowsheet(dynamic=False) m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) - m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp, reaction_package=m.fs.rp) + m.fs.cv = ControlVolume0DBlock(property_package=m.fs.pp) - m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) with pytest.raises(BalanceTypeNotSupportedError): m.fs.cv.add_isothermal_constraint() diff --git a/idaes/core/base/tests/test_control_volume_1d.py b/idaes/core/base/tests/test_control_volume_1d.py index 3ede162772..073ab5cce0 100644 --- a/idaes/core/base/tests/test_control_volume_1d.py +++ b/idaes/core/base/tests/test_control_volume_1d.py @@ -3447,12 +3447,9 @@ def test_add_isothermal_energy_balances(): m = ConcreteModel() m.fs = Flowsheet(dynamic=False) m.fs.pp = PhysicalParameterTestBlock() - m.fs.rp = ReactionParameterTestBlock(property_package=m.fs.pp) - m.fs.pp.del_component(m.fs.pp.phase_equilibrium_idx) m.fs.cv = ControlVolume1DBlock( property_package=m.fs.pp, - reaction_package=m.fs.rp, transformation_method="dae.finite_difference", transformation_scheme="BACKWARD", finite_elements=10, @@ -3460,7 +3457,6 @@ def test_add_isothermal_energy_balances(): m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=True) - m.fs.cv.add_reaction_blocks(has_equilibrium=False) with pytest.raises(BalanceTypeNotSupportedError): m.fs.cv.add_isothermal_constraint() From e54dcb5e96c151e9167e5db9de6b4787d9cb098b Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 15 Jan 2025 12:26:07 -0500 Subject: [PATCH 08/14] Typo in error messages --- idaes/core/base/extended_control_volume0d.py | 6 +++--- idaes/core/base/extended_control_volume1d.py | 6 +++--- idaes/core/base/tests/test_extended_control_volume_0d.py | 6 +++--- idaes/core/base/tests/test_extended_control_volume_1d.py | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/idaes/core/base/extended_control_volume0d.py b/idaes/core/base/extended_control_volume0d.py index 7c3f86b586..d774ce7faa 100644 --- a/idaes/core/base/extended_control_volume0d.py +++ b/idaes/core/base/extended_control_volume0d.py @@ -77,13 +77,13 @@ def add_isothermal_constraint( raise ConfigurationError( f"{self.name}: isothermal energy balance option requires that has_heat_transfer is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance and add a constraint to equate inlet and outlet temperatures." ) if has_work_transfer: raise ConfigurationError( f"{self.name}: isothermal energy balance option requires that has_work_transfer is False. " "If you are trying to solve for work under isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance and add a constraint to equate inlet and outlet temperatures." ) if has_enthalpy_transfer: raise ConfigurationError( @@ -93,7 +93,7 @@ def add_isothermal_constraint( raise ConfigurationError( f"{self.name}: isothermal energy balance option requires that has_heat_of_reaction is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance and add a constraint to equate inlet and outlet temperatures." ) if custom_term is not None: raise ConfigurationError( diff --git a/idaes/core/base/extended_control_volume1d.py b/idaes/core/base/extended_control_volume1d.py index 1fc4c90beb..6898710d8f 100644 --- a/idaes/core/base/extended_control_volume1d.py +++ b/idaes/core/base/extended_control_volume1d.py @@ -80,13 +80,13 @@ def add_isothermal_constraint( raise ConfigurationError( f"{self.name}: isothermal energy balance option requires that has_heat_transfer is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance and add a constraint to equate inlet and outlet temperatures." ) if has_work_transfer: raise ConfigurationError( f"{self.name}: isothermal energy balance option requires that has_work_transfer is False. " "If you are trying to solve for work under isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance and add a constraint to equate inlet and outlet temperatures." ) if has_enthalpy_transfer: raise ConfigurationError( @@ -96,7 +96,7 @@ def add_isothermal_constraint( raise ConfigurationError( f"{self.name}: isothermal energy balance option requires that has_heat_of_reaction is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures." + "a full energy balance and add a constraint to equate inlet and outlet temperatures." ) if custom_term is not None: raise ConfigurationError( diff --git a/idaes/core/base/tests/test_extended_control_volume_0d.py b/idaes/core/base/tests/test_extended_control_volume_0d.py index 5c558cbb45..2cd5934e21 100644 --- a/idaes/core/base/tests/test_extended_control_volume_0d.py +++ b/idaes/core/base/tests/test_extended_control_volume_0d.py @@ -98,7 +98,7 @@ def test_add_isothermal_constraint_heat_transfer(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_heat_transfer is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures.", + "a full energy balance and add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_heat_transfer=True) @@ -115,7 +115,7 @@ def test_add_isothermal_constraint_work_transfer(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_work_transfer is False. " "If you are trying to solve for work under isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures.", + "a full energy balance and add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_work_transfer=True) @@ -147,7 +147,7 @@ def test_add_isothermal_constraint_heat_of_rxn(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_heat_of_reaction is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures.", + "a full energy balance and add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_heat_of_reaction=True) diff --git a/idaes/core/base/tests/test_extended_control_volume_1d.py b/idaes/core/base/tests/test_extended_control_volume_1d.py index fa66873063..e6a6f84c9c 100644 --- a/idaes/core/base/tests/test_extended_control_volume_1d.py +++ b/idaes/core/base/tests/test_extended_control_volume_1d.py @@ -114,7 +114,7 @@ def test_add_isothermal_constraint_heat_transfer(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_heat_transfer is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures.", + "a full energy balance and add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_heat_transfer=True) @@ -136,7 +136,7 @@ def test_add_isothermal_constraint_work_transfer(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_work_transfer is False. " "If you are trying to solve for work under isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures.", + "a full energy balance and add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_work_transfer=True) @@ -178,7 +178,7 @@ def test_add_isothermal_constraint_heat_of_rxn(): ConfigurationError, match="fs.cv: isothermal energy balance option requires that has_heat_of_reaction is False. " "If you are trying to solve for heat duty to achieve isothermal operation, please use " - "a full energy balance as add a constraint to equate inlet and outlet temperatures.", + "a full energy balance and add a constraint to equate inlet and outlet temperatures.", ): m.fs.cv.add_isothermal_constraint(has_heat_of_reaction=True) From 88a03944e145c167d72d6d4569f5ef4057ea0c09 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 15 Jan 2025 13:48:05 -0500 Subject: [PATCH 09/14] Apply discretization in 1d tests --- .../tests/test_extended_control_volume_1d.py | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/idaes/core/base/tests/test_extended_control_volume_1d.py b/idaes/core/base/tests/test_extended_control_volume_1d.py index e6a6f84c9c..9c466e546e 100644 --- a/idaes/core/base/tests/test_extended_control_volume_1d.py +++ b/idaes/core/base/tests/test_extended_control_volume_1d.py @@ -30,6 +30,7 @@ from idaes.core.util.testing import ( PhysicalParameterTestBlock, ) +from idaes.core.util.model_diagnostics import DiagnosticsToolbox # ----------------------------------------------------------------------------- @@ -50,22 +51,35 @@ def test_add_isothermal_constraint(): property_package=m.fs.pp, transformation_method="dae.finite_difference", transformation_scheme="BACKWARD", - finite_elements=10, + finite_elements=4, ) m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_isothermal_constraint() + m.fs.cv.apply_transformation() assert isinstance(m.fs.cv.enthalpy_balances, Constraint) - assert len(m.fs.cv.enthalpy_balances) == 1 # x==0 is skipped + assert len(m.fs.cv.enthalpy_balances) == (5-1)*1 # x==0 so (5-1) spatial points and 1 time point + + assert (0, 0) not in m.fs.cv.enthalpy_balances + assert str(m.fs.cv.enthalpy_balances[0, 0.25].expr) == str( + m.fs.cv.properties[0, 0].temperature == m.fs.cv.properties[0, 0.25].temperature + ) + assert str(m.fs.cv.enthalpy_balances[0, 0.5].expr) == str( + m.fs.cv.properties[0, 0.25].temperature == m.fs.cv.properties[0, 0.5].temperature + ) + assert str(m.fs.cv.enthalpy_balances[0, 0.75].expr) == str( + m.fs.cv.properties[0, 0.5].temperature == m.fs.cv.properties[0, 0.75].temperature + ) assert str(m.fs.cv.enthalpy_balances[0, 1].expr) == str( m.fs.cv.properties[0, 0].temperature == m.fs.cv.properties[0, 1].temperature ) - assert (0, 0) not in m.fs.cv.enthalpy_balances - assert_units_consistent(m.fs.cv) + # Fix inlet and check for unit consistency and structural singularities + m.fs.cv.properties[0, 0].temperature.fix() + dt = DiagnosticsToolbox(m) + dt.assert_no_structural_warnings() @pytest.mark.unit @@ -78,23 +92,36 @@ def test_add_isothermal_constraint_dynamic(): property_package=m.fs.pp, transformation_method="dae.finite_difference", transformation_scheme="BACKWARD", - finite_elements=10, + finite_elements=4, ) m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_isothermal_constraint() + m.fs.cv.apply_transformation() assert isinstance(m.fs.cv.enthalpy_balances, Constraint) - assert len(m.fs.cv.enthalpy_balances) == 4 # x==0 is skipped + assert len(m.fs.cv.enthalpy_balances) == (5-1)*4 # x==0 so (5-1) spatial points and 4 time points + for t in m.fs.time: + assert (t, 0) not in m.fs.cv.enthalpy_balances + assert str(m.fs.cv.enthalpy_balances[t, 0.25].expr) == str( + m.fs.cv.properties[t, 0].temperature == m.fs.cv.properties[t, 0.25].temperature + ) + assert str(m.fs.cv.enthalpy_balances[t, 0.5].expr) == str( + m.fs.cv.properties[t, 0.25].temperature == m.fs.cv.properties[t, 0.5].temperature + ) + assert str(m.fs.cv.enthalpy_balances[t, 0.75].expr) == str( + m.fs.cv.properties[t, 0.5].temperature == m.fs.cv.properties[t, 0.75].temperature + ) assert str(m.fs.cv.enthalpy_balances[t, 1].expr) == str( m.fs.cv.properties[t, 0].temperature == m.fs.cv.properties[t, 1].temperature ) - assert (t, 0) not in m.fs.cv.enthalpy_balances - assert_units_consistent(m.fs.cv) + # Fix inlet and check for unit consistency and structural singularities + m.fs.cv.properties[:, 0].temperature.fix() + dt = DiagnosticsToolbox(m) + dt.assert_no_structural_warnings() @pytest.mark.unit From 7f2d9f9247d08a3c6bbd47bdf1f0d922329aed50 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Wed, 15 Jan 2025 13:53:56 -0500 Subject: [PATCH 10/14] Running black --- .../tests/test_extended_control_volume_1d.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/idaes/core/base/tests/test_extended_control_volume_1d.py b/idaes/core/base/tests/test_extended_control_volume_1d.py index 9c466e546e..efb140ca8b 100644 --- a/idaes/core/base/tests/test_extended_control_volume_1d.py +++ b/idaes/core/base/tests/test_extended_control_volume_1d.py @@ -60,17 +60,21 @@ def test_add_isothermal_constraint(): m.fs.cv.apply_transformation() assert isinstance(m.fs.cv.enthalpy_balances, Constraint) - assert len(m.fs.cv.enthalpy_balances) == (5-1)*1 # x==0 so (5-1) spatial points and 1 time point + assert ( + len(m.fs.cv.enthalpy_balances) == (5 - 1) * 1 + ) # x==0 so (5-1) spatial points and 1 time point assert (0, 0) not in m.fs.cv.enthalpy_balances assert str(m.fs.cv.enthalpy_balances[0, 0.25].expr) == str( m.fs.cv.properties[0, 0].temperature == m.fs.cv.properties[0, 0.25].temperature ) assert str(m.fs.cv.enthalpy_balances[0, 0.5].expr) == str( - m.fs.cv.properties[0, 0.25].temperature == m.fs.cv.properties[0, 0.5].temperature + m.fs.cv.properties[0, 0.25].temperature + == m.fs.cv.properties[0, 0.5].temperature ) assert str(m.fs.cv.enthalpy_balances[0, 0.75].expr) == str( - m.fs.cv.properties[0, 0.5].temperature == m.fs.cv.properties[0, 0.75].temperature + m.fs.cv.properties[0, 0.5].temperature + == m.fs.cv.properties[0, 0.75].temperature ) assert str(m.fs.cv.enthalpy_balances[0, 1].expr) == str( m.fs.cv.properties[0, 0].temperature == m.fs.cv.properties[0, 1].temperature @@ -101,18 +105,23 @@ def test_add_isothermal_constraint_dynamic(): m.fs.cv.apply_transformation() assert isinstance(m.fs.cv.enthalpy_balances, Constraint) - assert len(m.fs.cv.enthalpy_balances) == (5-1)*4 # x==0 so (5-1) spatial points and 4 time points + assert ( + len(m.fs.cv.enthalpy_balances) == (5 - 1) * 4 + ) # x==0 so (5-1) spatial points and 4 time points for t in m.fs.time: assert (t, 0) not in m.fs.cv.enthalpy_balances assert str(m.fs.cv.enthalpy_balances[t, 0.25].expr) == str( - m.fs.cv.properties[t, 0].temperature == m.fs.cv.properties[t, 0.25].temperature + m.fs.cv.properties[t, 0].temperature + == m.fs.cv.properties[t, 0.25].temperature ) assert str(m.fs.cv.enthalpy_balances[t, 0.5].expr) == str( - m.fs.cv.properties[t, 0.25].temperature == m.fs.cv.properties[t, 0.5].temperature + m.fs.cv.properties[t, 0.25].temperature + == m.fs.cv.properties[t, 0.5].temperature ) assert str(m.fs.cv.enthalpy_balances[t, 0.75].expr) == str( - m.fs.cv.properties[t, 0.5].temperature == m.fs.cv.properties[t, 0.75].temperature + m.fs.cv.properties[t, 0.5].temperature + == m.fs.cv.properties[t, 0.75].temperature ) assert str(m.fs.cv.enthalpy_balances[t, 1].expr) == str( m.fs.cv.properties[t, 0].temperature == m.fs.cv.properties[t, 1].temperature From 71245de6b196a07f9f63035d122f897f10821030 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 16 Jan 2025 11:09:30 -0500 Subject: [PATCH 11/14] Rename to isothermal constraint and add type hints --- idaes/core/base/extended_control_volume0d.py | 20 +++++++----- idaes/core/base/extended_control_volume1d.py | 20 +++++++----- .../tests/test_extended_control_volume_0d.py | 15 +++++---- .../tests/test_extended_control_volume_1d.py | 32 ++++++++++--------- 4 files changed, 49 insertions(+), 38 deletions(-) diff --git a/idaes/core/base/extended_control_volume0d.py b/idaes/core/base/extended_control_volume0d.py index d774ce7faa..ca1f656f67 100644 --- a/idaes/core/base/extended_control_volume0d.py +++ b/idaes/core/base/extended_control_volume0d.py @@ -16,6 +16,8 @@ __author__ = "Andrew Lee" +from pyomo.environ import Constraint, Expression + # Import IDAES cores from idaes.core.base.control_volume0d import ControlVolume0DBlockData from idaes.core import declare_process_block_class @@ -43,12 +45,12 @@ class ExtendedControlVolume0DBlockData(ControlVolume0DBlockData): def add_isothermal_constraint( self, - has_heat_of_reaction=False, - has_heat_transfer=False, - has_work_transfer=False, - has_enthalpy_transfer=False, - custom_term=None, - ): + has_heat_of_reaction: bool =False, + has_heat_transfer: bool =False, + has_work_transfer: bool =False, + has_enthalpy_transfer: bool =False, + custom_term: Expression =None, + ) -> Constraint: """ This method constructs an isothermal constraint for the control volume. @@ -101,6 +103,8 @@ def add_isothermal_constraint( ) # Add isothermal constraint - @self.Constraint(self.flowsheet().time, doc="Energy balances") - def enthalpy_balances(b, t): + @self.Constraint(self.flowsheet().time, doc="Isothermal constraint - replaces energy balance") + def isothermal_constraint(b, t): return b.properties_in[t].temperature == b.properties_out[t].temperature + + return self.isothermal_constraint diff --git a/idaes/core/base/extended_control_volume1d.py b/idaes/core/base/extended_control_volume1d.py index 6898710d8f..c8bddbdf8d 100644 --- a/idaes/core/base/extended_control_volume1d.py +++ b/idaes/core/base/extended_control_volume1d.py @@ -16,6 +16,8 @@ __author__ = "Andrew Lee" +from pyomo.environ import Constraint, Expression + # Import Pyomo libraries from pyomo.environ import Constraint @@ -46,12 +48,12 @@ class ExtendedControlVolume1DBlockData(ControlVolume1DBlockData): def add_isothermal_constraint( self, - has_heat_of_reaction=False, - has_heat_transfer=False, - has_work_transfer=False, - has_enthalpy_transfer=False, - custom_term=None, - ): + has_heat_of_reaction: bool =False, + has_heat_transfer: bool =False, + has_work_transfer: bool =False, + has_enthalpy_transfer: bool =False, + custom_term: Expression =None, + ) -> None: """ This method constructs an isothermal constraint for the control volume. @@ -105,9 +107,9 @@ def add_isothermal_constraint( # Add isothermal constraint @self.Constraint( - self.flowsheet().time, self.length_domain, doc="Energy balances" + self.flowsheet().time, self.length_domain, doc="Isothermal constraint - replaces energy balances" ) - def enthalpy_balances(b, t, x): + def isothermal_constraint(b, t, x): if x == b.length_domain.first(): return Constraint.Skip @@ -115,3 +117,5 @@ def enthalpy_balances(b, t, x): b.properties[t, b.length_domain.prev(x)].temperature == b.properties[t, x].temperature ) + + return self.isothermal_constraint \ No newline at end of file diff --git a/idaes/core/base/tests/test_extended_control_volume_0d.py b/idaes/core/base/tests/test_extended_control_volume_0d.py index 2cd5934e21..7bf71cca1b 100644 --- a/idaes/core/base/tests/test_extended_control_volume_0d.py +++ b/idaes/core/base/tests/test_extended_control_volume_0d.py @@ -51,11 +51,12 @@ def test_add_isothermal_constraint(): m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_isothermal_constraint() + cons = m.fs.cv.add_isothermal_constraint() - assert isinstance(m.fs.cv.enthalpy_balances, Constraint) - assert len(m.fs.cv.enthalpy_balances) == 1 - assert str(m.fs.cv.enthalpy_balances[0].expr) == str( + assert cons is m.fs.cv.isothermal_constraint + assert isinstance(m.fs.cv.isothermal_constraint, Constraint) + assert len(m.fs.cv.isothermal_constraint) == 1 + assert str(m.fs.cv.isothermal_constraint[0].expr) == str( m.fs.cv.properties_in[0].temperature == m.fs.cv.properties_out[0].temperature ) @@ -75,10 +76,10 @@ def test_add_isothermal_constraint_dynamic(): m.fs.cv.add_isothermal_constraint() - assert isinstance(m.fs.cv.enthalpy_balances, Constraint) - assert len(m.fs.cv.enthalpy_balances) == 4 + assert isinstance(m.fs.cv.isothermal_constraint, Constraint) + assert len(m.fs.cv.isothermal_constraint) == 4 for t in m.fs.time: - assert str(m.fs.cv.enthalpy_balances[t].expr) == str( + assert str(m.fs.cv.isothermal_constraint[t].expr) == str( m.fs.cv.properties_in[t].temperature == m.fs.cv.properties_out[t].temperature ) diff --git a/idaes/core/base/tests/test_extended_control_volume_1d.py b/idaes/core/base/tests/test_extended_control_volume_1d.py index efb140ca8b..90725bd940 100644 --- a/idaes/core/base/tests/test_extended_control_volume_1d.py +++ b/idaes/core/base/tests/test_extended_control_volume_1d.py @@ -56,27 +56,29 @@ def test_add_isothermal_constraint(): m.fs.cv.add_geometry() m.fs.cv.add_state_blocks(has_phase_equilibrium=False) - m.fs.cv.add_isothermal_constraint() + cons = m.fs.cv.add_isothermal_constraint() m.fs.cv.apply_transformation() - assert isinstance(m.fs.cv.enthalpy_balances, Constraint) + assert cons is m.fs.cv.isothermal_constraint + + assert isinstance(m.fs.cv.isothermal_constraint, Constraint) assert ( - len(m.fs.cv.enthalpy_balances) == (5 - 1) * 1 + len(m.fs.cv.isothermal_constraint) == (5 - 1) * 1 ) # x==0 so (5-1) spatial points and 1 time point - assert (0, 0) not in m.fs.cv.enthalpy_balances - assert str(m.fs.cv.enthalpy_balances[0, 0.25].expr) == str( + assert (0, 0) not in m.fs.cv.isothermal_constraint + assert str(m.fs.cv.isothermal_constraint[0, 0.25].expr) == str( m.fs.cv.properties[0, 0].temperature == m.fs.cv.properties[0, 0.25].temperature ) - assert str(m.fs.cv.enthalpy_balances[0, 0.5].expr) == str( + assert str(m.fs.cv.isothermal_constraint[0, 0.5].expr) == str( m.fs.cv.properties[0, 0.25].temperature == m.fs.cv.properties[0, 0.5].temperature ) - assert str(m.fs.cv.enthalpy_balances[0, 0.75].expr) == str( + assert str(m.fs.cv.isothermal_constraint[0, 0.75].expr) == str( m.fs.cv.properties[0, 0.5].temperature == m.fs.cv.properties[0, 0.75].temperature ) - assert str(m.fs.cv.enthalpy_balances[0, 1].expr) == str( + assert str(m.fs.cv.isothermal_constraint[0, 1].expr) == str( m.fs.cv.properties[0, 0].temperature == m.fs.cv.properties[0, 1].temperature ) @@ -104,26 +106,26 @@ def test_add_isothermal_constraint_dynamic(): m.fs.cv.add_isothermal_constraint() m.fs.cv.apply_transformation() - assert isinstance(m.fs.cv.enthalpy_balances, Constraint) + assert isinstance(m.fs.cv.isothermal_constraint, Constraint) assert ( - len(m.fs.cv.enthalpy_balances) == (5 - 1) * 4 + len(m.fs.cv.isothermal_constraint) == (5 - 1) * 4 ) # x==0 so (5-1) spatial points and 4 time points for t in m.fs.time: - assert (t, 0) not in m.fs.cv.enthalpy_balances - assert str(m.fs.cv.enthalpy_balances[t, 0.25].expr) == str( + assert (t, 0) not in m.fs.cv.isothermal_constraint + assert str(m.fs.cv.isothermal_constraint[t, 0.25].expr) == str( m.fs.cv.properties[t, 0].temperature == m.fs.cv.properties[t, 0.25].temperature ) - assert str(m.fs.cv.enthalpy_balances[t, 0.5].expr) == str( + assert str(m.fs.cv.isothermal_constraint[t, 0.5].expr) == str( m.fs.cv.properties[t, 0.25].temperature == m.fs.cv.properties[t, 0.5].temperature ) - assert str(m.fs.cv.enthalpy_balances[t, 0.75].expr) == str( + assert str(m.fs.cv.isothermal_constraint[t, 0.75].expr) == str( m.fs.cv.properties[t, 0.5].temperature == m.fs.cv.properties[t, 0.75].temperature ) - assert str(m.fs.cv.enthalpy_balances[t, 1].expr) == str( + assert str(m.fs.cv.isothermal_constraint[t, 1].expr) == str( m.fs.cv.properties[t, 0].temperature == m.fs.cv.properties[t, 1].temperature ) From 08e3983d4d59cefba6d294ca8125e4404a3228e8 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 16 Jan 2025 11:11:42 -0500 Subject: [PATCH 12/14] Running black --- idaes/core/base/extended_control_volume0d.py | 14 ++++++++------ idaes/core/base/extended_control_volume1d.py | 16 +++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/idaes/core/base/extended_control_volume0d.py b/idaes/core/base/extended_control_volume0d.py index ca1f656f67..d435cd0a99 100644 --- a/idaes/core/base/extended_control_volume0d.py +++ b/idaes/core/base/extended_control_volume0d.py @@ -45,11 +45,11 @@ class ExtendedControlVolume0DBlockData(ControlVolume0DBlockData): def add_isothermal_constraint( self, - has_heat_of_reaction: bool =False, - has_heat_transfer: bool =False, - has_work_transfer: bool =False, - has_enthalpy_transfer: bool =False, - custom_term: Expression =None, + has_heat_of_reaction: bool = False, + has_heat_transfer: bool = False, + has_work_transfer: bool = False, + has_enthalpy_transfer: bool = False, + custom_term: Expression = None, ) -> Constraint: """ This method constructs an isothermal constraint for the control volume. @@ -103,7 +103,9 @@ def add_isothermal_constraint( ) # Add isothermal constraint - @self.Constraint(self.flowsheet().time, doc="Isothermal constraint - replaces energy balance") + @self.Constraint( + self.flowsheet().time, doc="Isothermal constraint - replaces energy balance" + ) def isothermal_constraint(b, t): return b.properties_in[t].temperature == b.properties_out[t].temperature diff --git a/idaes/core/base/extended_control_volume1d.py b/idaes/core/base/extended_control_volume1d.py index c8bddbdf8d..3c099003d0 100644 --- a/idaes/core/base/extended_control_volume1d.py +++ b/idaes/core/base/extended_control_volume1d.py @@ -48,11 +48,11 @@ class ExtendedControlVolume1DBlockData(ControlVolume1DBlockData): def add_isothermal_constraint( self, - has_heat_of_reaction: bool =False, - has_heat_transfer: bool =False, - has_work_transfer: bool =False, - has_enthalpy_transfer: bool =False, - custom_term: Expression =None, + has_heat_of_reaction: bool = False, + has_heat_transfer: bool = False, + has_work_transfer: bool = False, + has_enthalpy_transfer: bool = False, + custom_term: Expression = None, ) -> None: """ This method constructs an isothermal constraint for the control volume. @@ -107,7 +107,9 @@ def add_isothermal_constraint( # Add isothermal constraint @self.Constraint( - self.flowsheet().time, self.length_domain, doc="Isothermal constraint - replaces energy balances" + self.flowsheet().time, + self.length_domain, + doc="Isothermal constraint - replaces energy balances", ) def isothermal_constraint(b, t, x): if x == b.length_domain.first(): @@ -118,4 +120,4 @@ def isothermal_constraint(b, t, x): == b.properties[t, x].temperature ) - return self.isothermal_constraint \ No newline at end of file + return self.isothermal_constraint From 05ee4979525c185713e1f35f96027586ff4e4f4a Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 16 Jan 2025 11:29:53 -0500 Subject: [PATCH 13/14] Fixing duplicated import --- idaes/core/base/extended_control_volume1d.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/idaes/core/base/extended_control_volume1d.py b/idaes/core/base/extended_control_volume1d.py index 3c099003d0..463edd7dfc 100644 --- a/idaes/core/base/extended_control_volume1d.py +++ b/idaes/core/base/extended_control_volume1d.py @@ -16,10 +16,8 @@ __author__ = "Andrew Lee" -from pyomo.environ import Constraint, Expression - # Import Pyomo libraries -from pyomo.environ import Constraint +from pyomo.environ import Constraint, Expression # Import IDAES cores from idaes.core.base.control_volume1d import ControlVolume1DBlockData From de153a70a47659802da1497fc8f41017cbec07f9 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 16 Jan 2025 12:42:18 -0500 Subject: [PATCH 14/14] Updaing docs and doc strings --- docs/reference_guides/core/control_volume_0d.rst | 2 +- docs/reference_guides/core/control_volume_1d.rst | 2 +- idaes/core/base/extended_control_volume0d.py | 2 +- idaes/core/base/extended_control_volume1d.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference_guides/core/control_volume_0d.rst b/docs/reference_guides/core/control_volume_0d.rst index 4149f80834..bd0c73b926 100644 --- a/docs/reference_guides/core/control_volume_0d.rst +++ b/docs/reference_guides/core/control_volume_0d.rst @@ -307,6 +307,6 @@ A constraint equating temperature at the inlet and outlet of the control volume **Constraints** -`enthalpy_balances(t)`: +`isothermal_constraint(t)`: .. math:: T_{in, t} == T_{out, t} diff --git a/docs/reference_guides/core/control_volume_1d.rst b/docs/reference_guides/core/control_volume_1d.rst index ba34b22834..3118f37557 100644 --- a/docs/reference_guides/core/control_volume_1d.rst +++ b/docs/reference_guides/core/control_volume_1d.rst @@ -336,7 +336,7 @@ A constraint equating temperature along the length domain of the control volume **Constraints** -`enthalpy_balances(t)`: +`isothermal_constraint(t, x)`: .. math:: T_{t, x-1} == T_{t, x} diff --git a/idaes/core/base/extended_control_volume0d.py b/idaes/core/base/extended_control_volume0d.py index d435cd0a99..e82b6842f1 100644 --- a/idaes/core/base/extended_control_volume0d.py +++ b/idaes/core/base/extended_control_volume0d.py @@ -73,7 +73,7 @@ def add_isothermal_constraint( Method should accept time and phase list as arguments. Returns: - Constraint object representing enthalpy balances + Constraint object representing isothermal constraint """ if has_heat_transfer: raise ConfigurationError( diff --git a/idaes/core/base/extended_control_volume1d.py b/idaes/core/base/extended_control_volume1d.py index 463edd7dfc..af47e6e0a2 100644 --- a/idaes/core/base/extended_control_volume1d.py +++ b/idaes/core/base/extended_control_volume1d.py @@ -74,7 +74,7 @@ def add_isothermal_constraint( Method should accept time and phase list as arguments. Returns: - Constraint object representing enthalpy balances + Constraint object representing isothermal constraints """ if has_heat_transfer: raise ConfigurationError(