From 4f77d52c5b6c9fa2f3aa7c77836e122822a1fd31 Mon Sep 17 00:00:00 2001 From: Ludovico Bianchi Date: Tue, 26 Nov 2024 13:00:05 -0600 Subject: [PATCH 1/6] Update conda install commands after idaes-pse was added to conda-forge (#1543) --- .github/workflows/integration.yml | 2 +- .../getting_started/install_templates/conda_idaes_pse.txt | 2 +- docs/tutorials/getting_started/install_templates/quickstart.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 38cf60dd05..adbc7f989f 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -248,7 +248,7 @@ jobs: install-target: ${{ matrix.pip-install-target }} - name: Remove dependencies installable with pip but not with conda # NOTE some dependencies that are installed by default with pip are not available through conda - # so they're not installed if IDAES is installed with `conda -c conda-forge -c IDAES-PSE idaes-pse` + # so they're not installed if IDAES is installed with `conda -c conda-forge idaes-pse` # to ensure this scenario is handled properly, since we don't have (yet) the conda-build process integrated with the CI, # we manually remove the "pip-but-not-conda" dependencies after installing with pip # as an approximation of the enviroment that we'd get from `conda install` diff --git a/docs/tutorials/getting_started/install_templates/conda_idaes_pse.txt b/docs/tutorials/getting_started/install_templates/conda_idaes_pse.txt index 302e795c2a..920490e270 100644 --- a/docs/tutorials/getting_started/install_templates/conda_idaes_pse.txt +++ b/docs/tutorials/getting_started/install_templates/conda_idaes_pse.txt @@ -7,7 +7,7 @@ We recommend using Conda to manage your environment & modules. conda activate my-idaes-env # Install IDAES Conda package - conda install --yes -c IDAES-PSE -c conda-forge idaes-pse + conda install --yes -c conda-forge idaes-pse .. note:: The command above will install the most recent stable (release) version of IDAES. To install other versions of IDAES, including pre-release versions, diff --git a/docs/tutorials/getting_started/install_templates/quickstart.txt b/docs/tutorials/getting_started/install_templates/quickstart.txt index 51cb475815..90525a6073 100644 --- a/docs/tutorials/getting_started/install_templates/quickstart.txt +++ b/docs/tutorials/getting_started/install_templates/quickstart.txt @@ -1,5 +1,5 @@ # Set up & activate Conda new environment with IDAES-PSE -conda create --yes --name my-idaes-env -c conda-forge -c IDAES-PSE python=3.10 idaes-pse +conda create --yes --name my-idaes-env -c conda-forge python=3.10 idaes-pse conda activate my-idaes-env # Install IDAES Extensions From 4bf597e0ae50f262e64d5c055c71d6a60fff5c29 Mon Sep 17 00:00:00 2001 From: Dan Gunter Date: Tue, 26 Nov 2024 11:01:49 -0800 Subject: [PATCH 2/6] Add a mapping for Pop!_OS (default for System76 laptops) to get-extensions (#1534) * outputs * config line * reverted --------- Co-authored-by: Ludovico Bianchi --- idaes/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/idaes/config.py b/idaes/config.py index 8acd5073bd..c784080244 100644 --- a/idaes/config.py +++ b/idaes/config.py @@ -69,6 +69,7 @@ "xubuntu1804": "ubuntu1804", "xubuntu2004": "ubuntu2004", "xubuntu2204": "ubuntu2204", + "pop22": "ubuntu2204", } # Machine map binary_arch_map = { From febf2c381992afb42b34aab916ccaceede74db3d Mon Sep 17 00:00:00 2001 From: Ludovico Bianchi Date: Wed, 27 Nov 2024 12:09:40 -0600 Subject: [PATCH 3/6] Shorten names of files causing installation errors (#1540) (#1545) --- ...tr_15481_Calcite_ST.onnx => net_Calcite_ST.onnx} | Bin ...aes_info.json => net_Calcite_ST_idaes_info.json} | 0 idaes/core/surrogate/tests/test_onnx_surrogate.py | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename idaes/core/surrogate/tests/data/onnx_models/{net_st_net_5000_STM_100_s_2000000_60_5_tanh_1e-06_4096_tr_15481_Calcite_ST.onnx => net_Calcite_ST.onnx} (100%) rename idaes/core/surrogate/tests/data/onnx_models/{net_st_net_5000_STM_100_s_2000000_60_5_tanh_1e-06_4096_tr_15481_Calcite_ST_idaes_info.json => net_Calcite_ST_idaes_info.json} (100%) diff --git a/idaes/core/surrogate/tests/data/onnx_models/net_st_net_5000_STM_100_s_2000000_60_5_tanh_1e-06_4096_tr_15481_Calcite_ST.onnx b/idaes/core/surrogate/tests/data/onnx_models/net_Calcite_ST.onnx similarity index 100% rename from idaes/core/surrogate/tests/data/onnx_models/net_st_net_5000_STM_100_s_2000000_60_5_tanh_1e-06_4096_tr_15481_Calcite_ST.onnx rename to idaes/core/surrogate/tests/data/onnx_models/net_Calcite_ST.onnx diff --git a/idaes/core/surrogate/tests/data/onnx_models/net_st_net_5000_STM_100_s_2000000_60_5_tanh_1e-06_4096_tr_15481_Calcite_ST_idaes_info.json b/idaes/core/surrogate/tests/data/onnx_models/net_Calcite_ST_idaes_info.json similarity index 100% rename from idaes/core/surrogate/tests/data/onnx_models/net_st_net_5000_STM_100_s_2000000_60_5_tanh_1e-06_4096_tr_15481_Calcite_ST_idaes_info.json rename to idaes/core/surrogate/tests/data/onnx_models/net_Calcite_ST_idaes_info.json diff --git a/idaes/core/surrogate/tests/test_onnx_surrogate.py b/idaes/core/surrogate/tests/test_onnx_surrogate.py index 623254c285..e4ae7d44ce 100644 --- a/idaes/core/surrogate/tests/test_onnx_surrogate.py +++ b/idaes/core/surrogate/tests/test_onnx_surrogate.py @@ -40,7 +40,7 @@ def load_onnx_model_data( - name="net_st_net_5000_STM_100_s_2000000_60_5_tanh_1e-06_4096_tr_15481_Calcite_ST", + name="net_Calcite_ST", ): onnx_folder_name = os.path.join(this_file_dir(), "data", "onnx_models") onnx_model = onnx.load(os.path.join(onnx_folder_name, "{}.onnx".format(name))) @@ -138,7 +138,7 @@ def test_onnx_surrogate_load_and_save_from_file(): onnx_surrogate = ONNXSurrogate.load_onnx_model( onnx_model_location=os.path.join(this_file_dir(), "data", "onnx_models"), - model_name="net_st_net_5000_STM_100_s_2000000_60_5_tanh_1e-06_4096_tr_15481_Calcite_ST", + model_name="net_Calcite_ST", ) with TempfileManager.new_context() as tf: dname = tf.mkdtemp() From b032b22c85bb7e1777b2a1788ad845ff1e7d353f Mon Sep 17 00:00:00 2001 From: Morgan Wang <45240736+Morgan88888888@users.noreply.github.com> Date: Wed, 27 Nov 2024 16:01:16 -0500 Subject: [PATCH 4/6] 1D Membrane Model for CO2 Capture and Utilization (#1378) * Add CCUS file structure * One-dimensional membrane model * add the unit test file * reformatted by black * reformat using black * fixed the path issue in testing * added the missing heading * refined the workspace name * formatting issue resolved for GitHub test * fixed typo * fixed the unit model importing issue * fixed linter warnings * resolved the comments * fix linter issues * formated * remove the unit level property config * save changes * added test for different configs and added stream table display * added more docs to explain the models and settings * Addressed the comments to support different property packages * add linebreak * corrected copyright info * reformatted file to pass test * fixed copyright info * added material conservation test * reformatted * address comments * added basic documentation * fix pylint test * fix doc strings * fix the doc string * added what the inputs/degrees of freedom * fix pytest --------- Co-authored-by: Keith Beattie Co-authored-by: Ludovico Bianchi --- .../model_libraries/models_extra/index.rst | 2 + .../membrane_model/1d_membrane.rst | 39 +++ .../co2_capture_and_utilization/__init__.py | 0 .../unit_models/README.md | 1 + .../unit_models/__init__.py | 13 + .../unit_models/membrane_1d.py | 301 +++++++++++++++++ .../unit_models/tests/__init__.py | 0 .../unit_models/tests/test_membrane_1d.py | 304 ++++++++++++++++++ 8 files changed, 660 insertions(+) create mode 100644 docs/reference_guides/model_libraries/models_extra/membrane_model/1d_membrane.rst create mode 100644 idaes/models_extra/co2_capture_and_utilization/__init__.py create mode 100644 idaes/models_extra/co2_capture_and_utilization/unit_models/README.md create mode 100644 idaes/models_extra/co2_capture_and_utilization/unit_models/__init__.py create mode 100644 idaes/models_extra/co2_capture_and_utilization/unit_models/membrane_1d.py create mode 100644 idaes/models_extra/co2_capture_and_utilization/unit_models/tests/__init__.py create mode 100644 idaes/models_extra/co2_capture_and_utilization/unit_models/tests/test_membrane_1d.py diff --git a/docs/reference_guides/model_libraries/models_extra/index.rst b/docs/reference_guides/model_libraries/models_extra/index.rst index 069761e4a7..2f932d0981 100644 --- a/docs/reference_guides/model_libraries/models_extra/index.rst +++ b/docs/reference_guides/model_libraries/models_extra/index.rst @@ -6,3 +6,5 @@ Additional IDAES Model Libraries phe temperature_swing_adsorption/fixed_bed_tsa0d + membrane_model/1d_membrane + diff --git a/docs/reference_guides/model_libraries/models_extra/membrane_model/1d_membrane.rst b/docs/reference_guides/model_libraries/models_extra/membrane_model/1d_membrane.rst new file mode 100644 index 0000000000..678ca9858d --- /dev/null +++ b/docs/reference_guides/model_libraries/models_extra/membrane_model/1d_membrane.rst @@ -0,0 +1,39 @@ +One-dimensional membrane class for CO2 gas separation +================================================================ + +This is a one-dimensional model for gas separation in CO₂ capture applications. +The model will be discretized in the flow direction, and it supports two flow patterns: +counter-current flow and co-current flow. The model was customized for gas-phase separation +in CO₂ capture with a single-layer design. If a multi-layer design is needed, multiple units +can be connected for this application. The two sides of the membrane are called the feed side +and sweep side. The sweep stream inlet is optional. The driving force across the membrane is the +partial pressure difference in this gas separation application. Additionally, the energy balance +assumes that temperature remains constant on each side of the membrane. + +Variables +--------- + +Model Inputs - symbol: + +* Membrane length - :math:`L` +* Membrane Area - :math:`A` +* Permeance - :math:`per` +* Feed flowrate - :math:`F_fr` +* Feed compositions - :math:`x` +* Feed pressure - :math:`P` +* Feed temperature - :math:`T` + + +Model Outputs : + +* Permeate compositions +* Permeate flowrate + +Degrees of Freedom +------------------ + +The DOF should be 0 for square problem simulations. + + + + diff --git a/idaes/models_extra/co2_capture_and_utilization/__init__.py b/idaes/models_extra/co2_capture_and_utilization/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/idaes/models_extra/co2_capture_and_utilization/unit_models/README.md b/idaes/models_extra/co2_capture_and_utilization/unit_models/README.md new file mode 100644 index 0000000000..f31b88e6d7 --- /dev/null +++ b/idaes/models_extra/co2_capture_and_utilization/unit_models/README.md @@ -0,0 +1 @@ +This directory contains the unit models for Carbon Capture and Utilization diff --git a/idaes/models_extra/co2_capture_and_utilization/unit_models/__init__.py b/idaes/models_extra/co2_capture_and_utilization/unit_models/__init__.py new file mode 100644 index 0000000000..fae1ee123d --- /dev/null +++ b/idaes/models_extra/co2_capture_and_utilization/unit_models/__init__.py @@ -0,0 +1,13 @@ +################################################################################# +# 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. +################################################################################# +from .membrane_1d import Membrane1D, MembraneFlowPattern diff --git a/idaes/models_extra/co2_capture_and_utilization/unit_models/membrane_1d.py b/idaes/models_extra/co2_capture_and_utilization/unit_models/membrane_1d.py new file mode 100644 index 0000000000..9feb1f1e8e --- /dev/null +++ b/idaes/models_extra/co2_capture_and_utilization/unit_models/membrane_1d.py @@ -0,0 +1,301 @@ +################################################################################# +# 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. +################################################################################# + +""" +One-dimensional membrane class for CO2 gas separation +""" + + +from enum import Enum +from pyomo.common.config import Bool, ConfigDict, ConfigValue, In +from pyomo.environ import ( + Param, + Var, + units, + Expression, +) +from pyomo.network import Port + +from idaes.core import ( + FlowDirection, + UnitModelBlockData, + declare_process_block_class, + useDefault, + MaterialFlowBasis, +) +from idaes.core.util.config import is_physical_parameter_block +from idaes.models.unit_models.mscontactor import MSContactor +from idaes.core.util.exceptions import ConfigurationError +from idaes.core.util.tables import create_stream_table_dataframe + +__author__ = "Maojian Wang" + + +class MembraneFlowPattern(Enum): + """ + Enum of supported flow patterns for membrane. + So far only support countercurrent and cocurrent flow + """ + + COUNTERCURRENT = 1 + COCURRENT = 2 + + +@declare_process_block_class("Membrane1D") +class Membrane1DData(UnitModelBlockData): + """Standard Membrane 1D Unit Model Class.""" + + CONFIG = UnitModelBlockData.CONFIG() + + Stream_Config = ConfigDict() + + Stream_Config.declare( + "property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for given stream", + doc="""Property parameter object used to define property calculations for given stream, + **default** - useDefault. + **Valid values:** { + **useDefault** - use default package from parent model or flowsheet, + **PhysicalParameterObject** - a PhysicalParameterBlock object.}""", + ), + ) + Stream_Config.declare( + "property_package_args", + ConfigDict( + implicit=True, + description="Dict of arguments to use for constructing property package", + doc="""A ConfigDict with arguments to be passed to property block(s) + and used when constructing these, + **default** - None. + **Valid values:** { + see property package for documentation.}""", + ), + ) + + Stream_Config.declare( + "has_energy_balance", + ConfigValue( + default=True, + domain=Bool, + doc="Bool indicating whether to include energy balance for stream. Default=True.", + ), + ) + Stream_Config.declare( + "has_pressure_balance", + ConfigValue( + default=True, + domain=Bool, + doc="Bool indicating whether to include pressure balance for stream. Default=True.", + ), + ) + + CONFIG.declare( + "sweep_flow", + ConfigValue( + default=True, + domain=Bool, + doc="Bool indicating whether there is a sweep flow in the permeate side.", + description="Bool indicating whether stream has a feed Port and inlet " + "state, or if all flow is provided via mass transfer. Default=True.", + ), + ) + CONFIG.declare( + "finite_elements", + ConfigValue( + default=5, + domain=int, + description="Number of finite elements in length domain", + doc="""Number of finite elements to use when discretizing length + domain (default=5)""", + ), + ) + CONFIG.declare( + "flow_type", + ConfigValue( + default=MembraneFlowPattern.COUNTERCURRENT, + domain=In(MembraneFlowPattern), + description="Flow configuration of membrane", + doc="""Flow configuration of membrane + MembraneFlowPattern.COCURRENT - feed and sweep flows from 0 to 1 + MembraneFlowPattern.COUNTERCURRENT - feed side flows from 0 to 1 and sweep side flows from 1 to 0 (default)""", + ), + ) + + for side_name in ["feed", "sweep"]: + CONFIG.declare( + side_name + "_side", + Stream_Config(), + ) + + def build(self): + """ + This is a one-dimensional model for gas separation in CO₂ capture applications. + The model will be discretized in the flow direction, and it supports two flow patterns: + counter-current flow and co-current flow. The model was customized for gas-phase separation + in CO₂ capture with a single-layer design. If a multi-layer design is needed, multiple units + can be connected for this application. The two sides of the membrane are called the feed side + and sweep side. The sweep stream inlet is optional. The driving force across the membrane is the + partial pressure difference in this gas separation application. Additionally, the energy balance + assumes that temperature remains constant on each side of the membrane. + + """ + super().build() + + feed_dict = dict(self.config.feed_side) + sweep_dict = dict(self.config.sweep_side) + + feed_dict["flow_direction"] = FlowDirection.forward + if self.config.flow_type == MembraneFlowPattern.COCURRENT: + sweep_dict["flow_direction"] = FlowDirection.forward + elif self.config.flow_type == MembraneFlowPattern.COUNTERCURRENT: + sweep_dict["flow_direction"] = FlowDirection.backward + else: + raise ConfigurationError( + f"{self.name} Membrane1D only supports cocurrent and " + "countercurrent flow patterns, but flow_type configuration" + " argument was set to {config.flow_type}." + ) + + if self.config.sweep_flow is False: + sweep_dict["has_feed"] = False + + streams_dict = {"feed_side": feed_dict, "sweep_side": sweep_dict} + self.mscontactor = MSContactor( + streams=streams_dict, + number_of_finite_elements=self.config.finite_elements, + ) + + self.feed_side_inlet = Port(extends=self.mscontactor.feed_side_inlet) + self.feed_side_outlet = Port(extends=self.mscontactor.feed_side_outlet) + if self.config.sweep_flow is True: + self.sweep_side_inlet = Port(extends=self.mscontactor.sweep_side_inlet) + self.sweep_side_outlet = Port(extends=self.mscontactor.sweep_side_outlet) + + self._make_geometry() + self._make_performance() + + def _make_geometry(self): + + self.area = Var( + initialize=100, units=units.cm**2, doc="Area per cell (or finite element)" + ) + + self.length = Var(initialize=100, units=units.cm, doc="The membrane length") + self.cell_length = Expression(expr=self.length / self.config.finite_elements) + + self.cell_area = Var(initialize=100, units=units.cm**2, doc="The membrane area") + + @self.Constraint() + def area_per_cell(self): + return self.cell_area == self.area / self.config.finite_elements + + def _make_performance(self): + feed_side_units = ( + self.config.feed_side.property_package.get_metadata().derived_units + ) + crossover_component_list = list( + set(self.mscontactor.feed_side.component_list) + & set(self.mscontactor.sweep_side.component_list) + ) + + self.permeance = Var( + self.flowsheet().time, + self.mscontactor.elements, + crossover_component_list, + initialize=1, + doc="Values in Gas Permeance Unit (GPU)", + units=units.dimensionless, + ) + + self.gpu_factor = Param( + default=10e-8 / 13333.2239, + units=units.m / units.s / units.Pa, + mutable=True, + # This is a coefficient that will convert the unit of permeability from GPU to SI units for further calculation" + ) + + p_units = feed_side_units.PRESSURE + + @self.Constraint( + self.flowsheet().time, + self.mscontactor.elements, + crossover_component_list, + doc="permeability calculation", + ) + def permeability_calculation(self, t, s, m): + feed_side_state = self.mscontactor.feed_side[t, s] + if feed_side_state.get_material_flow_basis() is MaterialFlowBasis.molar: + mb_units = feed_side_units.FLOW_MOLE + rho = self.mscontactor.feed_side[t, s].dens_mol + elif feed_side_state.get_material_flow_basis() is MaterialFlowBasis.mass: + mb_units = feed_side_units.FLOW_MASS + rho = self.mscontactor.feed_side[t, s].dens_mass + else: + raise TypeError( + "This model only supports MaterialFlowBasis equal to molar or mass" + ) + + return self.mscontactor.material_transfer_term[ + t, s, "feed_side", "sweep_side", m + ] == -units.convert( + ( + rho + * self.gpu_factor + * self.permeance[t, s, m] + * self.cell_area + * ( + self.mscontactor.feed_side[t, s].pressure + * self.mscontactor.feed_side[t, s].mole_frac_comp[m] + - units.convert( + self.mscontactor.sweep_side[t, s].pressure, to_units=p_units + ) + * self.mscontactor.sweep_side[t, s].mole_frac_comp[m] + ) + ), + to_units=mb_units, + ) + + @self.Constraint( + self.flowsheet().time, + self.mscontactor.elements, + doc="isothermal constraint", + ) + def isothermal_constraint(self, t, s): + return ( + self.mscontactor.feed_side[t, s].temperature + == self.mscontactor.sweep_side[t, s].temperature + ) + + def _get_stream_table_contents(self, time_point=0): + if self.config.sweep_flow: + return create_stream_table_dataframe( + { + "Feed Inlet": self.feed_side_inlet, + "Feed Outlet": self.feed_side_outlet, + "Permeate Inlet": self.sweep_side_inlet, + "Permeate Outlet": self.sweep_side_outlet, + }, + time_point=time_point, + ) + else: + return create_stream_table_dataframe( + { + "Feed Inlet": self.feed_side_inlet, + "Feed Outlet": self.feed_side_outlet, + "Permeate Outlet": self.sweep_side_outlet, + }, + time_point=time_point, + ) diff --git a/idaes/models_extra/co2_capture_and_utilization/unit_models/tests/__init__.py b/idaes/models_extra/co2_capture_and_utilization/unit_models/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/idaes/models_extra/co2_capture_and_utilization/unit_models/tests/test_membrane_1d.py b/idaes/models_extra/co2_capture_and_utilization/unit_models/tests/test_membrane_1d.py new file mode 100644 index 0000000000..8c129ebffe --- /dev/null +++ b/idaes/models_extra/co2_capture_and_utilization/unit_models/tests/test_membrane_1d.py @@ -0,0 +1,304 @@ +################################################################################# +# 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 Membrane 1D model +""" +__author__ = "Maojian Wang" + +# pylint: disable=unused-import +import pytest + +from pyomo.environ import ( + check_optimal_termination, + assert_optimal_termination, + ConcreteModel, + value, +) +from idaes.core import FlowsheetBlock +from idaes.core.util.model_statistics import ( + number_variables, + number_total_constraints, + number_unused_variables, +) +from idaes.core.solvers import get_solver +from idaes.core.initialization import ( + BlockTriangularizationInitializer, +) +from idaes.core.util import DiagnosticsToolbox +from idaes.models_extra.power_generation.properties.natural_gas_PR import ( + get_prop, + EosType, +) +from idaes.models.properties.modular_properties.base.generic_property import ( + GenericParameterBlock, +) +from idaes.models_extra.co2_capture_and_utilization.unit_models import ( + Membrane1D, + MembraneFlowPattern, +) + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +# ----------------------------------------------------------------------------- +@pytest.mark.unit +def test_config_countercurrent(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = GenericParameterBlock( + **get_prop(["CO2", "H2O", "N2"], ["Vap"], eos=EosType.IDEAL), + doc="Key flue gas property parameters", + ) + + m.fs.unit = Membrane1D( + finite_elements=3, + dynamic=False, + sweep_flow=True, + flow_type=MembraneFlowPattern.COUNTERCURRENT, + feed_side={"property_package": m.fs.properties}, + sweep_side={"property_package": m.fs.properties}, + ) + + # Check unit config arguments + assert len(m.fs.unit.config) == 7 + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + + +@pytest.mark.unit +def test_congif_cocurrent(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = GenericParameterBlock( + **get_prop(["CO2", "H2O", "N2"], ["Vap"], eos=EosType.IDEAL), + doc="Key flue gas property parameters", + ) + + m.fs.unit = Membrane1D( + finite_elements=3, + dynamic=False, + sweep_flow=True, + flow_type=MembraneFlowPattern.COCURRENT, + feed_side={"property_package": m.fs.properties}, + sweep_side={"property_package": m.fs.properties}, + ) + + # Check unit config arguments + assert len(m.fs.unit.config) == 7 + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + + +class TestMembrane: + @pytest.fixture(scope="class") + def membrane(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = GenericParameterBlock( + **get_prop(["CO2", "H2O", "N2"], ["Vap"], eos=EosType.IDEAL), + doc="Key flue gas property parameters", + ) + m.fs.unit = Membrane1D( + finite_elements=3, + dynamic=False, + sweep_flow=True, + flow_type=MembraneFlowPattern.COUNTERCURRENT, + feed_side={"property_package": m.fs.properties}, + sweep_side={"property_package": m.fs.properties}, + ) + + m.fs.unit.permeance[:, :, "CO2"].fix(1500) + m.fs.unit.permeance[:, :, "H2O"].fix(1500 / 25) + m.fs.unit.permeance[:, :, "N2"].fix(1500 / 25) + m.fs.unit.area.fix(100) + m.fs.unit.length.fix(10) + + m.fs.unit.feed_side_inlet.flow_mol[0].fix(100) + m.fs.unit.feed_side_inlet.temperature[0].fix(365) + m.fs.unit.feed_side_inlet.pressure[0].fix(120000) + m.fs.unit.feed_side_inlet.mole_frac_comp[0, "N2"].fix(0.76) + m.fs.unit.feed_side_inlet.mole_frac_comp[0, "CO2"].fix(0.13) + m.fs.unit.feed_side_inlet.mole_frac_comp[0, "H2O"].fix(0.11) + + m.fs.unit.sweep_side_inlet.flow_mol[0].fix(0.01) + m.fs.unit.sweep_side_inlet.temperature[0].fix(300) + m.fs.unit.sweep_side_inlet.pressure[0].fix(51325) + m.fs.unit.sweep_side_inlet.mole_frac_comp[0, "H2O"].fix(0.9986) + m.fs.unit.sweep_side_inlet.mole_frac_comp[0, "CO2"].fix(0.0003) + m.fs.unit.sweep_side_inlet.mole_frac_comp[0, "N2"].fix(0.0001) + + return m + + @pytest.mark.build + @pytest.mark.unit + def test_build(self, membrane): + assert hasattr(membrane.fs.unit, "feed_side_inlet") + assert len(membrane.fs.unit.feed_side_inlet.vars) == 4 + assert hasattr(membrane.fs.unit.feed_side_inlet, "flow_mol") + assert hasattr(membrane.fs.unit.feed_side_inlet, "mole_frac_comp") + assert hasattr(membrane.fs.unit.feed_side_inlet, "temperature") + assert hasattr(membrane.fs.unit.feed_side_inlet, "pressure") + + assert hasattr(membrane.fs.unit, "sweep_side_inlet") + assert len(membrane.fs.unit.sweep_side_inlet.vars) == 4 + assert hasattr(membrane.fs.unit.sweep_side_inlet, "flow_mol") + assert hasattr(membrane.fs.unit.sweep_side_inlet, "mole_frac_comp") + assert hasattr(membrane.fs.unit.sweep_side_inlet, "temperature") + assert hasattr(membrane.fs.unit.sweep_side_inlet, "pressure") + + assert hasattr(membrane.fs.unit, "feed_side_outlet") + assert len(membrane.fs.unit.feed_side_outlet.vars) == 4 + assert hasattr(membrane.fs.unit.feed_side_outlet, "flow_mol") + assert hasattr(membrane.fs.unit.feed_side_outlet, "mole_frac_comp") + assert hasattr(membrane.fs.unit.feed_side_outlet, "temperature") + assert hasattr(membrane.fs.unit.feed_side_outlet, "pressure") + + assert hasattr(membrane.fs.unit, "sweep_side_outlet") + assert len(membrane.fs.unit.sweep_side_outlet.vars) == 4 + assert hasattr(membrane.fs.unit.sweep_side_outlet, "flow_mol") + assert hasattr(membrane.fs.unit.sweep_side_outlet, "mole_frac_comp") + assert hasattr(membrane.fs.unit.sweep_side_outlet, "temperature") + assert hasattr(membrane.fs.unit.sweep_side_outlet, "pressure") + + assert hasattr(membrane.fs.unit, "mscontactor") + assert hasattr(membrane.fs.unit, "permeability_calculation") + assert hasattr(membrane.fs.unit, "isothermal_constraint") + + assert number_variables(membrane) == 157 + assert number_total_constraints(membrane) == 89 + assert number_unused_variables(membrane) == 28 + + @pytest.mark.component + def test_structural_issues(self, membrane): + dt = DiagnosticsToolbox(membrane) + dt.assert_no_structural_warnings() + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solve(self, membrane): + initializer = BlockTriangularizationInitializer(constraint_tolerance=2e-5) + initializer.initialize(membrane.fs.unit) + results = solver.solve(membrane) + # Check for optimal solution + assert_optimal_termination(results) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, membrane): + dt = DiagnosticsToolbox(membrane) + dt.assert_no_numerical_warnings() + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_feed_solution(self, membrane): + assert pytest.approx(99.99, abs=1e-2) == value( + membrane.fs.unit.feed_side_outlet.flow_mol[0] + ) + assert pytest.approx(0.1299, abs=1e-4) == value( + membrane.fs.unit.feed_side_outlet.mole_frac_comp[0, "CO2"] + ) + assert pytest.approx(0.11, abs=1e-4) == value( + membrane.fs.unit.feed_side_outlet.mole_frac_comp[0, "H2O"] + ) + assert pytest.approx(0.76, abs=1e-4) == value( + membrane.fs.unit.feed_side_outlet.mole_frac_comp[0, "N2"] + ) + + assert pytest.approx(365, abs=1e-2) == value( + membrane.fs.unit.feed_side_outlet.temperature[0] + ) + assert pytest.approx(120000, abs=1e-2) == value( + membrane.fs.unit.feed_side_outlet.pressure[0] + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_sweep_side_solution(self, membrane): + assert pytest.approx(0.01006, abs=1e-4) == value( + membrane.fs.unit.sweep_side_outlet.flow_mol[0] + ) + assert pytest.approx(0.0070, abs=1e-4) == value( + membrane.fs.unit.sweep_side_outlet.mole_frac_comp[0, "CO2"] + ) + assert pytest.approx(0.99120, abs=1e-4) == value( + membrane.fs.unit.sweep_side_outlet.mole_frac_comp[0, "H2O"] + ) + assert pytest.approx(0.001710, abs=1e-4) == value( + membrane.fs.unit.sweep_side_outlet.mole_frac_comp[0, "N2"] + ) + + assert pytest.approx(365, abs=1e-2) == value( + membrane.fs.unit.sweep_side_outlet.temperature[0] + ) + assert pytest.approx(51325.0, abs=1e-2) == value( + membrane.fs.unit.sweep_side_outlet.pressure[0] + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_enthalpy_balance(self, membrane): + + assert ( + abs( + value( + ( + membrane.fs.unit.feed_side_inlet.flow_mol[0] + * membrane.fs.unit.mscontactor.feed_side_inlet_state[ + 0 + ].enth_mol_phase["Vap"] + + membrane.fs.unit.sweep_side_inlet.flow_mol[0] + * membrane.fs.unit.mscontactor.sweep_side_inlet_state[ + 0 + ].enth_mol_phase["Vap"] + - membrane.fs.unit.feed_side_outlet.flow_mol[0] + * membrane.fs.unit.mscontactor.feed_side[0, 3].enth_mol_phase[ + "Vap" + ] + - membrane.fs.unit.sweep_side_outlet.flow_mol[0] + * membrane.fs.unit.mscontactor.sweep_side[0, 1].enth_mol_phase[ + "Vap" + ] + ) + ) + ) + <= 1e-6 + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_material_balance(self, membrane): + + assert ( + abs( + value( + ( + membrane.fs.unit.feed_side_inlet.flow_mol[0] + + membrane.fs.unit.sweep_side_inlet.flow_mol[0] + - membrane.fs.unit.feed_side_outlet.flow_mol[0] + - membrane.fs.unit.sweep_side_outlet.flow_mol[0] + ) + ) + ) + <= 1e-3 + ) From e8c9712e1c99faefe6d9b7f338e369d331781179 Mon Sep 17 00:00:00 2001 From: Tanner Polley Date: Wed, 27 Nov 2024 15:33:01 -0700 Subject: [PATCH 5/6] Adding StreamScaler unit model, along with the test and rst file. (#1517) * pull from MEA development branch * Added test file for StreamScaler unit model and added the unit model to the init file * pull from MEA development branch * Added test file for StreamScaler unit model and added the unit model to the init file * Added rst file for Stream Scaler Unit Model * Added stream_scaler.rst to the table of contents * Created the rst file for the stream_scaler unit model * Changed the default timeout time from 180 seconds -> 360 seconds due to build.py timing out * Reformatted with Black * Removed unused code flagged by Pylint * Removed unused code flagged by Pylint * Removed unused code flagged by Pylint * Removed unused code flagged by Pylint * Added StreamScalerData class to the init file for unit models * Added Tanner Polley as a co-author * Adjusted the details of the additional constraints * Resolved the test_solution method for btx and removed the comment for no solve or numerical tests to be done for the iapws since the unit model has no unit model level contraints. Tried to run the test_solve but got no feasible solution * Ran Black on these files * Fixed degrees of freedom on doc file * Revised the documentation for Degrees of Freedom for Stream Scaler * Removed unnecessary comments --------- Co-authored-by: Doug A Co-authored-by: dallan-keylogic <88728506+dallan-keylogic@users.noreply.github.com> Co-authored-by: Ludovico Bianchi --- docs/build.py | 4 +- .../generic/unit_models/index.rst | 1 + .../generic/unit_models/stream_scaler.rst | 46 +++ idaes/models/unit_models/__init__.py | 1 + idaes/models/unit_models/stream_scaler.py | 244 ++++++++++++ .../unit_models/tests/test_stream_scaler.py | 364 ++++++++++++++++++ 6 files changed, 658 insertions(+), 2 deletions(-) create mode 100644 docs/reference_guides/model_libraries/generic/unit_models/stream_scaler.rst create mode 100644 idaes/models/unit_models/stream_scaler.py create mode 100644 idaes/models/unit_models/tests/test_stream_scaler.py diff --git a/docs/build.py b/docs/build.py index 52ad82e441..004576a381 100644 --- a/docs/build.py +++ b/docs/build.py @@ -213,8 +213,8 @@ def main() -> int: "-t", "--timeout", dest="timeout", - help="Timeout (in seconds) for sphinx-build (default=180)", - default=180, + help="Timeout (in seconds) for sphinx-build (default=360)", + default=360, type=int, ) prs.add_argument( diff --git a/docs/reference_guides/model_libraries/generic/unit_models/index.rst b/docs/reference_guides/model_libraries/generic/unit_models/index.rst index 40f6746e7b..c988b29c9c 100644 --- a/docs/reference_guides/model_libraries/generic/unit_models/index.rst +++ b/docs/reference_guides/model_libraries/generic/unit_models/index.rst @@ -27,6 +27,7 @@ Unit Models skeleton_unit statejunction stoichiometric_reactor + stream_scaler translator turbine valve diff --git a/docs/reference_guides/model_libraries/generic/unit_models/stream_scaler.rst b/docs/reference_guides/model_libraries/generic/unit_models/stream_scaler.rst new file mode 100644 index 0000000000..13a71068fe --- /dev/null +++ b/docs/reference_guides/model_libraries/generic/unit_models/stream_scaler.rst @@ -0,0 +1,46 @@ +Stream Scaler Block +=================== + +Stream Scaler Blocks are used to adjust size of streams to represent, for example, a stream being split across several identical units, which are then all modeled as a single IDAES unit + +Degrees of Freedom +------------------ + +Stream Scaler blocks have one degree of freedom (beyond the state variables in the ``StateBlock`` properties), a ``Var`` called ``multiplier``. It is the factor by which extensive state variables (defined as those having "flow" in their name) are scaled, with ``output_var = multiplier * input_var``. + +Model Structure +--------------- + +Stream Scaler Blocks consists of a single ``StateBlock`` (named properties), each with an inlet and outlet port. + +Additional Constraints +---------------------- + +Stream Scaler Blocks write no additional constraints* (besides those naturally occurring in ``StateBlocks``). + +Variables +--------- + +Stream Scaler blocks add no additional Variables. + +.. module:: idaes.models.unit_models.stream_scaler + + +Initialization +-------------- + +.. autoclass:: StreamScalerInitializer + :members: initialization_routine + +StreamScaler Class +------------------ + +.. autoclass:: StreamScaler + :members: + +StreamScalerData Class +---------------------- + +.. autoclass:: StreamScalerData + :members: + diff --git a/idaes/models/unit_models/__init__.py b/idaes/models/unit_models/__init__.py index 7b5397d290..6cf96a7cf0 100644 --- a/idaes/models/unit_models/__init__.py +++ b/idaes/models/unit_models/__init__.py @@ -40,6 +40,7 @@ ) from .shell_and_tube_1d import ShellAndTube1D, ShellAndTubeInitializer from .skeleton_model import SkeletonUnitModel, SkeletonUnitModelData +from .stream_scaler import StreamScaler, StreamScalerData from .statejunction import StateJunction, StateJunctionInitializer from .stoichiometric_reactor import StoichiometricReactor from .translator import Translator diff --git a/idaes/models/unit_models/stream_scaler.py b/idaes/models/unit_models/stream_scaler.py new file mode 100644 index 0000000000..1b6455eb0d --- /dev/null +++ b/idaes/models/unit_models/stream_scaler.py @@ -0,0 +1,244 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# +""" +Unit model to adjust size of streams to represent, for example, a stream being split across several identical units, +which are then all modeled as a single IDAES unit +""" +from functools import partial + +from pyomo.environ import ( + Block, + PositiveReals, + units as pyunits, + Var, +) +from pyomo.network import Port +from pyomo.common.config import ConfigBlock, ConfigValue, In + +from idaes.core import ( + declare_process_block_class, + UnitModelBlockData, + useDefault, +) +from idaes.core.util.config import ( + is_physical_parameter_block, +) +from idaes.core.base.var_like_expression import VarLikeExpression +from idaes.core.util.tables import create_stream_table_dataframe +import idaes.core.util.scaling as iscale +import idaes.logger as idaeslog + +from idaes.models.unit_models.feed import FeedInitializer as StreamScalerInitializer + +__author__ = "Douglas Allan, Tanner Polley" + + +# Set up logger +_log = idaeslog.getLogger(__name__) + + +@declare_process_block_class("StreamScaler") +class StreamScalerData(UnitModelBlockData): + """ + Unit model to adjust size of streams to represent, for example, a stream being split across several identical units, + which are then all modeled as a single IDAES unit + """ + + default_initializer = StreamScalerInitializer + + CONFIG = ConfigBlock() + CONFIG.declare( + "dynamic", + ConfigValue( + domain=In([False]), + default=False, + description="Dynamic model flag - must be False", + doc="""Indicates whether this model will be dynamic or not, + **default** = False. Scaler blocks are always steady-state.""", + ), + ) + CONFIG.declare( + "has_holdup", + ConfigValue( + default=False, + domain=In([False]), + description="Holdup construction flag - must be False", + doc="Scaler blocks do not contain holdup, thus this must be False.", + ), + ) + CONFIG.declare( + "property_package", + ConfigValue( + default=useDefault, + domain=is_physical_parameter_block, + description="Property package to use for StreamScaler", + doc="""Property parameter object used to define property +calculations, **default** - useDefault. +**Valid values:** { +**useDefault** - use default package from parent model or flowsheet, +**PropertyParameterObject** - a PropertyParameterBlock object.}""", + ), + ) + CONFIG.declare( + "property_package_args", + ConfigBlock( + implicit=True, + description="Arguments to use for constructing property packages", + doc="""A ConfigBlock with arguments to be passed to a property +block(s) and used when constructing these, +**default** - None. +**Valid values:** { +see property package for documentation.}""", + ), + ) + + def build(self): + """ + General build method for StreamScalerData. This method calls a number + of sub-methods which automate the construction of expected attributes + of unit models. + + Inheriting models should call `super().build`. + + Args: + None + + Returns: + None + """ + # Call super.build() + super(StreamScalerData, self).build() + + tmp_dict = dict(**self.config.property_package_args) + tmp_dict["has_phase_equilibrium"] = False + tmp_dict["defined_state"] = True + + # Call setup methods from ControlVolumeBlockData + self._get_property_package() + self._get_indexing_sets() + + self.properties = self.config.property_package.build_state_block( + self.flowsheet().time, doc="Material properties at inlet", **tmp_dict + ) + self.scaled_expressions = Block() + self.multiplier = Var( + initialize=1, + domain=PositiveReals, + units=pyunits.dimensionless, + doc="Factor by which to scale dimensionless streams", + ) + self.add_inlet_port(name="inlet", block=self.properties) + self.outlet = Port(doc="Outlet port") + + def rule_scale_var(b, *args, var=None): + return self.multiplier * var[args] + + def rule_no_scale_var(b, *args, var=None): + return var[args] + + for var_name in self.inlet.vars.keys(): + var = getattr(self.inlet, var_name) + if "flow" in var_name: + rule = partial(rule_scale_var, var=var) + else: + rule = partial(rule_no_scale_var, var=var) + self.scaled_expressions.add_component( + var_name, VarLikeExpression(var.index_set(), rule=rule) + ) + expr = getattr(self.scaled_expressions, var_name) + self.outlet.add(expr, var_name) + + def initialize_build( + blk, outlvl=idaeslog.NOTSET, optarg=None, solver=None, hold_state=False + ): + """ + Initialization routine for StreamScaler. + + Keyword Arguments: + outlvl : sets output level of initialization routine + optarg : solver options dictionary object (default=None, use + default solver options) + solver : str indicating which solver to use during + initialization (default = None, use default solver) + hold_state : flag indicating whether the initialization routine + should unfix any state variables fixed during + initialization, **default** - False. **Valid values:** + **True** - states variables are not unfixed, and a dict of + returned containing flags for which states were fixed + during initialization, **False** - state variables are + unfixed after initialization by calling the release_state + method. + + Returns: + If hold_states is True, returns a dict containing flags for which + states were fixed during initialization. + """ + + # Create solver + + # Initialize inlet state blocks + flags = blk.properties.initialize( + outlvl=outlvl, + optarg=optarg, + solver=solver, + hold_state=True, + ) + + if hold_state is True: + return flags + else: + blk.release_state(flags, outlvl=outlvl) + + def release_state(blk, flags, outlvl=idaeslog.NOTSET): + """ + Method to release state variables fixed during initialization. + + Keyword Arguments: + flags : dict containing information of which state variables + were fixed during initialization, and should now be + unfixed. This dict is returned by initialize if + hold_state = True. + outlvl : sets output level of logging + + Returns: + None + """ + blk.properties.release_state(flags, outlvl=outlvl) + + def _get_stream_table_contents(self, time_point=0): + io_dict = { + "Inlet": self.inlet, + # "Outlet": self.outlet, + } + return create_stream_table_dataframe(io_dict, time_point=time_point) + + def calculate_scaling_factors(self): + # Scaling factors for the property block are calculated automatically + super().calculate_scaling_factors() + + # Need to pass on scaling factors from the property block to the outlet + # VarLikeExpressions so arcs get scaled right + if self.multiplier.value == 0: + default = 1 + else: + default = 1 / self.multiplier.value + + scale = iscale.get_scaling_factor( + self.multiplier, default=default, warning=False + ) + for var_name in self.inlet.vars.keys(): + var = getattr(self.inlet, var_name) + outlet_expr = getattr(self.outlet, var_name) + for key, subvar in var.items(): + sf = iscale.get_scaling_factor(subvar, default=1, warning=True) + iscale.set_scaling_factor(outlet_expr[key], scale * sf) diff --git a/idaes/models/unit_models/tests/test_stream_scaler.py b/idaes/models/unit_models/tests/test_stream_scaler.py new file mode 100644 index 0000000000..608c91be4b --- /dev/null +++ b/idaes/models/unit_models/tests/test_stream_scaler.py @@ -0,0 +1,364 @@ +################################################################################# +# 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 Stream Scaler unit model. + +Author: Tanner Polley +""" + +import pytest +import pandas +from numpy import number + +from pyomo.environ import ( + check_optimal_termination, + ConcreteModel, + value, + units as pyunits, +) + +from idaes.core import FlowsheetBlock +from idaes.models.unit_models.stream_scaler import StreamScaler, StreamScalerInitializer + +from idaes.models.properties.activity_coeff_models.BTX_activity_coeff_VLE import ( + BTXParameterBlock, +) + +from idaes.models.properties import iapws95 +from idaes.models.properties.examples.saponification_thermo import ( + SaponificationParameterBlock, +) + +from idaes.core.util.model_statistics import ( + number_variables, + number_total_constraints, + number_unused_variables, + variables_set, +) +from idaes.core.util.testing import PhysicalParameterTestBlock, initialization_tester +from idaes.core.solvers import get_solver +from idaes.core.initialization import ( + BlockTriangularizationInitializer, + InitializationStatus, +) +from idaes.core.util import DiagnosticsToolbox + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver("ipopt_v2") + + +# ----------------------------------------------------------------------------- +@pytest.mark.unit +def test_config(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = PhysicalParameterTestBlock() + + m.fs.unit = StreamScaler(property_package=m.fs.properties) + + # Check unit config arguments + assert len(m.fs.unit.config) == 4 + + assert not m.fs.unit.config.dynamic + assert not m.fs.unit.config.has_holdup + assert m.fs.unit.config.property_package is m.fs.properties + + assert m.fs.unit.default_initializer is StreamScalerInitializer + + +class TestSaponification(object): + @pytest.fixture(scope="class") + def sapon(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = SaponificationParameterBlock() + m.fs.unit = StreamScaler(property_package=m.fs.properties) + m.fs.unit.multiplier.fix(1) + + m.fs.unit.inlet.flow_vol.fix(1.0e-03) + m.fs.unit.inlet.conc_mol_comp[0, "H2O"].fix(55388.0) + m.fs.unit.inlet.conc_mol_comp[0, "NaOH"].fix(100.0) + m.fs.unit.inlet.conc_mol_comp[0, "EthylAcetate"].fix(100.0) + m.fs.unit.inlet.conc_mol_comp[0, "SodiumAcetate"].fix(1e-8) + m.fs.unit.inlet.conc_mol_comp[0, "Ethanol"].fix(1e-8) + + m.fs.unit.inlet.temperature.fix(303.15) + m.fs.unit.inlet.pressure.fix(101325.0) + return m + + @pytest.mark.build + @pytest.mark.unit + def test_build(self, sapon): + + assert hasattr(sapon.fs.unit, "inlet") + assert len(sapon.fs.unit.inlet.vars) == 4 + assert hasattr(sapon.fs.unit.inlet, "flow_vol") + assert hasattr(sapon.fs.unit.inlet, "conc_mol_comp") + assert hasattr(sapon.fs.unit.inlet, "temperature") + assert hasattr(sapon.fs.unit.inlet, "pressure") + + assert number_variables(sapon) == 9 + assert number_total_constraints(sapon) == 0 + assert number_unused_variables(sapon) == 9 + + @pytest.mark.component + def test_structural_issues(self, sapon): + dt = DiagnosticsToolbox(sapon) + dt.assert_no_structural_warnings() + + @pytest.mark.ui + @pytest.mark.unit + def test_get_performance_contents(self, sapon): + perf_dict = sapon.fs.unit._get_performance_contents() + + assert perf_dict is None + + @pytest.mark.ui + @pytest.mark.unit + def test_get_stream_table_contents(self, sapon): + stable = sapon.fs.unit._get_stream_table_contents() + + expected = pandas.DataFrame.from_dict( + { + "Units": { + "Volumetric Flowrate": getattr( + pyunits.pint_registry, "m**3/second" + ), + "Molar Concentration H2O": getattr( + pyunits.pint_registry, "mole/m**3" + ), + "Molar Concentration NaOH": getattr( + pyunits.pint_registry, "mole/m**3" + ), + "Molar Concentration EthylAcetate": getattr( + pyunits.pint_registry, "mole/m**3" + ), + "Molar Concentration SodiumAcetate": getattr( + pyunits.pint_registry, "mole/m**3" + ), + "Molar Concentration Ethanol": getattr( + pyunits.pint_registry, "mole/m**3" + ), + "Temperature": getattr(pyunits.pint_registry, "K"), + "Pressure": getattr(pyunits.pint_registry, "Pa"), + }, + "Inlet": { + "Volumetric Flowrate": 1e-3, + "Molar Concentration H2O": 55388, + "Molar Concentration NaOH": 100.00, + "Molar Concentration EthylAcetate": 100.00, + "Molar Concentration SodiumAcetate": 0, + "Molar Concentration Ethanol": 0, + "Temperature": 303.15, + "Pressure": 1.0132e05, + }, + } + ) + + pandas.testing.assert_frame_equal(stable, expected, rtol=1e-4, atol=1e-4) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_initialize(self, sapon): + initialization_tester(sapon) + + # No solve or numerical tests, as StreamScaler block has nothing to solve + + +class TestBTX(object): + @pytest.fixture(scope="class") + def btx(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = BTXParameterBlock(valid_phase="Liq") + m.fs.unit = StreamScaler(property_package=m.fs.properties) + m.fs.unit.multiplier.fix(1) + m.fs.unit.inlet.flow_mol[0].fix(5) # mol/s + m.fs.unit.inlet.temperature[0].fix(365) # K + m.fs.unit.inlet.pressure[0].fix(101325) # Pa + m.fs.unit.inlet.mole_frac_comp[0, "benzene"].fix(0.5) + m.fs.unit.inlet.mole_frac_comp[0, "toluene"].fix(0.5) + return m + + @pytest.mark.build + @pytest.mark.unit + def test_build(self, btx): + + assert hasattr(btx.fs.unit, "inlet") + assert len(btx.fs.unit.inlet.vars) == 4 + assert hasattr(btx.fs.unit.inlet, "flow_mol") + assert hasattr(btx.fs.unit.inlet, "mole_frac_comp") + assert hasattr(btx.fs.unit.inlet, "temperature") + assert hasattr(btx.fs.unit.inlet, "pressure") + + assert number_variables(btx) == 9 + assert number_total_constraints(btx) == 3 + assert number_unused_variables(btx) == 3 + + @pytest.mark.component + def test_structural_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_structural_warnings() + + @pytest.mark.ui + @pytest.mark.unit + def test_get_performance_contents(self, btx): + perf_dict = btx.fs.unit._get_performance_contents() + + assert perf_dict is None + + @pytest.mark.ui + @pytest.mark.unit + def test_get_stream_table_contents(self, btx): + stable = btx.fs.unit._get_stream_table_contents() + + expected = pandas.DataFrame.from_dict( + { + "Units": { + "flow_mol": getattr(pyunits.pint_registry, "mole/second"), + "mole_frac_comp benzene": getattr( + pyunits.pint_registry, "dimensionless" + ), + "mole_frac_comp toluene": getattr( + pyunits.pint_registry, "dimensionless" + ), + "temperature": getattr(pyunits.pint_registry, "kelvin"), + "pressure": getattr(pyunits.pint_registry, "Pa"), + }, + "Inlet": { + "flow_mol": 5.0, + "mole_frac_comp benzene": 0.5, + "mole_frac_comp toluene": 0.5, + "temperature": 365, + "pressure": 101325.0, + }, + } + ) + + pandas.testing.assert_frame_equal(stable, expected, rtol=1e-4, atol=1e-4) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_initialize(self, btx): + initialization_tester(btx) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solve(self, btx): + results = solver.solve(btx) + + # Check for optimal solution + assert check_optimal_termination(results) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solution(self, btx): + assert pytest.approx(5, abs=1e-3) == value(btx.fs.unit.inlet.flow_mol[0]) + assert pytest.approx(0.5, abs=1e-3) == value( + btx.fs.unit.inlet.mole_frac_comp[0, "benzene"] + ) + assert pytest.approx(0.5, abs=1e-3) == value( + btx.fs.unit.inlet.mole_frac_comp[0, "toluene"] + ) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_numerical_issues(self, btx): + dt = DiagnosticsToolbox(btx) + dt.assert_no_numerical_warnings() + + +# ----------------------------------------------------------------------------- +@pytest.mark.iapws +@pytest.mark.skipif(not iapws95.iapws95_available(), reason="IAPWS not available") +class TestIAPWS(object): + @pytest.fixture(scope="class") + def iapws(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = iapws95.Iapws95ParameterBlock() + + m.fs.unit = StreamScaler(property_package=m.fs.properties) + + m.fs.unit.multiplier.fix(1) + m.fs.unit.inlet.flow_mol[0].fix(100) + m.fs.unit.inlet.enth_mol[0].fix(5000) + m.fs.unit.inlet.pressure[0].fix(101325) + + return m + + @pytest.mark.build + @pytest.mark.unit + def test_build(self, iapws): + assert len(iapws.fs.unit.inlet.vars) == 3 + assert hasattr(iapws.fs.unit.inlet, "flow_mol") + assert hasattr(iapws.fs.unit.inlet, "enth_mol") + assert hasattr(iapws.fs.unit.inlet, "pressure") + + assert number_variables(iapws) == 4 + assert number_total_constraints(iapws) == 0 + assert number_unused_variables(iapws) == 4 + + @pytest.mark.component + def test_structural_issues(self, iapws): + dt = DiagnosticsToolbox(iapws) + dt.assert_no_structural_warnings() + + @pytest.mark.ui + @pytest.mark.unit + def test_get_performance_contents(self, iapws): + perf_dict = iapws.fs.unit._get_performance_contents() + + assert perf_dict is None + + @pytest.mark.ui + @pytest.mark.unit + def test_get_stream_table_contents(self, iapws): + stable = iapws.fs.unit._get_stream_table_contents() + + expected = pandas.DataFrame.from_dict( + { + "Units": { + "Molar Flow": getattr(pyunits.pint_registry, "mole/second"), + "Mass Flow": getattr(pyunits.pint_registry, "kg/second"), + "T": getattr(pyunits.pint_registry, "K"), + "P": getattr(pyunits.pint_registry, "Pa"), + "Vapor Fraction": getattr(pyunits.pint_registry, "dimensionless"), + "Molar Enthalpy": getattr(pyunits.pint_registry, "J/mole"), + }, + "Inlet": { + "Molar Flow": 100, + "Mass Flow": 1.8015, + "T": 339.43, + "P": 101325, + "Vapor Fraction": 0, + "Molar Enthalpy": 5000, + }, + } + ) + + pandas.testing.assert_frame_equal(stable, expected, rtol=1e-4, atol=1e-4) + + @pytest.mark.solver + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_initialize(self, iapws): + initialization_tester(iapws) From 327d8f3f50022f3afb9e1c7862c4f6c759db38f1 Mon Sep 17 00:00:00 2001 From: Ludovico Bianchi Date: Wed, 27 Nov 2024 16:43:19 -0600 Subject: [PATCH 6/6] 2.8.dev0 --- idaes/ver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/ver.py b/idaes/ver.py index a6b79f80d2..598dfc7140 100644 --- a/idaes/ver.py +++ b/idaes/ver.py @@ -184,7 +184,7 @@ def git_hash(): pass #: Package's version as an object -package_version = Version(2, 7, 0, "development", 0, gh) +package_version = Version(2, 8, 0, "development", 0, gh) #: Package's version as a simple string __version__ = str(package_version)