diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 36a11a7d30..bbc4546687 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -109,6 +109,7 @@ jobs: with: activate-environment: ${{ env.IDAES_CONDA_ENV_NAME_DEV }} python-version: ${{ matrix.python-version }} + miniforge-version: latest - name: Set up idaes uses: ./.github/actions/setup-idaes with: @@ -167,6 +168,7 @@ jobs: with: activate-environment: ${{ env.IDAES_CONDA_ENV_NAME_DEV }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + miniforge-version: latest - name: Set up idaes uses: ./.github/actions/setup-idaes with: @@ -201,6 +203,7 @@ jobs: with: activate-environment: ${{ env.IDAES_CONDA_ENV_NAME_DEV }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + miniforge-version: latest - name: Set up idaes uses: ./.github/actions/setup-idaes with: @@ -233,6 +236,7 @@ jobs: with: activate-environment: ${{ env.IDAES_CONDA_ENV_NAME_DEV }} python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + miniforge-version: latest - name: Set up idaes uses: ./.github/actions/setup-idaes with: diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 913cdbef51..adbc7f989f 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -149,6 +149,7 @@ jobs: with: activate-environment: ${{ env.IDAES_CONDA_ENV_NAME_DEV }} python-version: ${{ matrix.python-version }} + miniforge-version: latest - name: Set up idaes uses: ./.github/actions/setup-idaes with: @@ -186,6 +187,7 @@ jobs: with: activate-environment: ${{ env.IDAES_CONDA_ENV_NAME_DEV }} python-version: ${{ matrix.python-version }} + miniforge-version: latest - name: Set up idaes uses: ./.github/actions/setup-idaes with: @@ -239,13 +241,14 @@ jobs: with: activate-environment: idaes-env python-version: ${{ env.DEFAULT_PYTHON_VERSION }} + miniforge-version: latest - name: Set up idaes (non-editable installation) uses: ./.github/actions/setup-idaes with: 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/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index a4dac7f486..0000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,148 +0,0 @@ -/* -############################################################################### -# 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. -############################################################################### - -This file defines the build/test/post steps for Jenkins. - -Dependencies: failed-test-email.template // This parses the output of Jenkins and formats the email that Jenkins sends -*/ - -def email_to = "idaes.jenkins@lbl.gov" // The email address that the build email will go to -def email_reply_to = "mrshepherd@lbl.gov" // The email address that will be in the reply-to - -pipeline { - agent { - docker { - image 'conda/miniconda3-centos7:latest' - } - } - stages { - // Commented out for an example until we start using these parameters - // stage('cron-nightly-test') { - // when { - // expression { params.BUILD_SCHEDULE == 'Nightly'} - // } - // steps { - // sh 'echo "nightly works"' - // } - // } - // stage('cron-weekly-test') { - // when { - // expression { params.BUILD_SCHEDULE == 'Weekly'} - // } - // steps { - // sh 'echo "weekly works"' - // } - // } - stage('root-setup') { - steps { - // slackSend (message: "Build Started - ${env.JOB_NAME} ${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)") - sh 'mkdir -p $JENKINS_HOME/email-templates' - sh 'cp failed-test-email.template $JENKINS_HOME/email-templates' - sh 'yum install -y gcc g++ git gcc-gfortran libboost-dev make' - sh 'pwd' - - } - } - stage('3.6-setup') { - when { - expression { params.PYTHON_VERSION == '3.6'} - } - steps { - sh ''' - conda create -n idaes3.6 python=3.6 pytest - source activate idaes3.6 - pip install -r requirements-dev.txt --user jenkins - export TEMP_LC_ALL=$LC_ALL - export TEMP_LANG=$LANG - export LC_ALL=en_US.utf-8 - export LANG=en_US.utf-8 - python setup.py install - idaes get-extensions - export LC_ALL=$TEMP_LC_ALL - export LANG=$TEMP_LANG - source deactivate - ''' - } - } - stage('3.7-setup') { - when { - expression { params.PYTHON_VERSION == '3.7'} - } - steps { - sh ''' - conda create -n idaes3.7 python=3.7 pytest - source activate idaes3.7 - pip install -r requirements-dev.txt --user jenkins - export TEMP_LC_ALL=$LC_ALL - export TEMP_LANG=$LANG - export LC_ALL=en_US.utf-8 - export LANG=en_US.utf-8 - python setup.py install - idaes get-extensions - export LC_ALL=$TEMP_LC_ALL - export LANG=$TEMP_LANG - source deactivate - ''' - } - } - stage('3.6-test') { - when { - expression { params.PYTHON_VERSION == '3.6'} - } - steps { - catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { - sh ''' - source activate idaes3.6 - pylint -E --ignore-patterns="test_.*" idaes || true - pytest --junitxml=results.xml -c pytest.ini idaes - source deactivate - ''' - } - } - } - stage('3.7-test') { - when { - expression { params.PYTHON_VERSION == '3.7'} - } - steps { - catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { - sh ''' - source activate idaes3.7 - pylint -E --ignore-patterns="test_.*" idaes || true - pytest --junitxml=results.xml -c pytest.ini idaes - source deactivate - ''' - } - } - } - } - post { - always { - junit allowEmptyResults: true, testResults: 'results.xml' - emailext attachLog: true, body: '''${SCRIPT, template="failed-test-email.template"}''', replyTo: '${email_reply_to}', - subject: "${JOB_NAME} ${params.PYTHON_VERSION} - Build ${BUILD_NUMBER} ${currentBuild.result}", to: '${email_to}' - } - // success { - // slackSend (color: '#00FF00', message: "SUCCESSFUL - ${env.JOB_NAME} ${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)") - // emailext attachLog: true, body: "${currentBuild.result}: ${BUILD_URL}", compressLog: true, replyTo: 'mrshepherd@lbl.gov', - // subject: "Build Log: ${JOB_NAME} - Build ${BUILD_NUMBER} ${currentBuild.result}", to: 'mrshepherd@lbl.gov' - // } - - // failure { - // slackSend (color: '#FF0000', message: "FAILED - ${env.JOB_NAME} ${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)") - // emailext attachLog: true, body: "${currentBuild.result}: ${env.BUILD_URL}", compressLog: true, replyTo: 'mrshepherd@lbl.gov', - // subject: "Build Log: ${env.JOB_NAME} - Build ${env.BUILD_NUMBER} ${currentBuild.result}", to: 'mrshepherd@lbl.gov' - // } - } -} diff --git a/README.md b/README.md index 2d5ee719b9..da9c1b779a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ scale-up, operation and troubleshooting of innovative, advanced energy systems. Our [complete documentation is online](https://idaes-pse.readthedocs.io/en/stable/) but here is a summarized set of steps to get started using the framework. For help and assistance, please visit the [IDAES PSE Discussions Board](https://github.com/IDAES/idaes-pse/discussions). -While not required, we encourage the installation of [Anaconda](https://www.anaconda.com/products/individual#Downloads) or [Miniconda](https://docs.conda.io/en/latest/miniconda.html) and using the `conda` command to create a separate python environment in which to install the IDAES Toolkit. +While not required, we encourage the installation of [Miniforge](https://conda-forge.org/miniforge/) with which you can use the `conda` command to create a separate python environment in which to install the IDAES Toolkit. Use conda to create a new "idaes-pse" (could be any name you like) environment then activate that environment: ```bash diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index ed27323fdf..0000000000 --- a/docker/README.md +++ /dev/null @@ -1 +0,0 @@ -Various IDAES related docker containers live here, each in their own subdir. diff --git a/docker/idaes-jupyterhub/Dockerfile b/docker/idaes-jupyterhub/Dockerfile deleted file mode 100644 index 3ecf922a0e..0000000000 --- a/docker/idaes-jupyterhub/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -# This Dockerfile is adapted (with modifications) from this Dockerfile: -# https://github.com/jupyter/docker-stacks/blob/master/scipy-notebook/Dockerfile - -ARG BASE_CONTAINER=jupyter/minimal-notebook -FROM $BASE_CONTAINER - -MAINTAINER Project IDAES - -USER $NB_UID - -# Install Python 3 packages -RUN conda install --quiet --yes 'ipywidgets' 'xlrd' && \ - # Activate ipywidgets extension in the environment that runs the notebook server - jupyter nbextension enable --py widgetsnbextension --sys-prefix && \ - # Also activate ipywidgets extension for JupyterLab - # Check this URL for most recent compatibilities - # https://github.com/jupyter-widgets/ipywidgets/tree/master/packages/jupyterlab-manager - jupyter labextension install @jupyter-widgets/jupyterlab-manager && \ - npm cache clean --force && \ - rm -rf $CONDA_DIR/share/jupyter/lab/staging && \ - rm -rf /home/$NB_USER/.cache/yarn && \ - rm -rf /home/$NB_USER/.node-gyp && \ - fix-permissions $CONDA_DIR && \ - fix-permissions /home/$NB_USER - -# Maintainer Note: We're using bokeh for plotting (not matplotlib). Uncomment if matplotlib needed. -# Import matplotlib the first time to build the font cache. -# ENV XDG_CACHE_HOME /home/$NB_USER/.cache/ -# RUN MPLBACKEND=Agg python -c "import matplotlib.pyplot" && \ -# fix-permissions /home/$NB_USER - -# Add top-level idaes source directory and change permissions to the notebook user: -ADD . /home/idaes -USER root -RUN sudo apt-get update -RUN sudo apt-get -y install libgfortran3 -RUN echo "America/Los_Angeles" > /etc/timezone -RUN chown -R $NB_UID /home/idaes - -# Install idaes requirements.txt -USER $NB_UID -WORKDIR /home/idaes -RUN pip install -r requirements-dev.txt -RUN python setup.py install -RUN idaes get-extensions - -WORKDIR /home -ENV PATH=$PATH:/home/jovyan/.idaes/bin -ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/jovyan/.idaes/lib:/opt/conda/lib/ -USER $NB_UID \ No newline at end of file diff --git a/docker/idaes-jupyterhub/README.md b/docker/idaes-jupyterhub/README.md deleted file mode 100644 index 2217fe80ed..0000000000 --- a/docker/idaes-jupyterhub/README.md +++ /dev/null @@ -1,10 +0,0 @@ -[![docker pulls](https://img.shields.io/docker/pulls/idaes/jupyterhub.svg)](https://hub.docker.com/r/idaes/jupyterhub/) [![docker stars](https://img.shields.io/docker/stars/idaes/jupyterhub.svg)](https://hub.docker.com/r/idaes/jupyterhub/) [![image metadata](https://images.microbadger.com/badges/image/idaes/jupyterhub.svg)](https://microbadger.com/images/idaes/jupyterhub "idaes/jupyterhub image metadata") - -## IDAES PSE image for jupyterhub - -This container is used for automated test of the IDAES PSE Framework. - -Please visit the documentation site for help using and contributing to this image and others. - -* [IDAES PSE on ReadTheDocs](https://idaes-pse.readthedocs.io/en/stable/) -* [Docker container build instruction](https://github.com/IDAES/idaes-dev/wiki/Building-Docker-image) (private link) diff --git a/docker/ubuntu-conda/Dockerfile b/docker/ubuntu-conda/Dockerfile deleted file mode 100644 index c21bd9c2f0..0000000000 --- a/docker/ubuntu-conda/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM ubuntu:18.04 -MAINTAINER IDAES Tech Team - -ARG IUSER=idaes - -RUN echo "\n____ INSTALL PACKAGES ___\n" \ - && apt-get -qq update \ - && apt-get -qq -y upgrade \ - && apt-get -qq -y install curl bzip2 locales \ - && apt-get -qq -y install build-essential libgfortran4 liblapack-dev \ - && apt-get -qq -y install openssh-client git \ - && apt-get -qq -y autoremove \ - && apt-get autoclean \ - && rm -rf /var/lib/apt/lists/* /var/log/dpkg.log - -RUN echo "\n___ SET LOCALE AND TIMEZONE ___\n" \ - && echo "Etc/UTC" > /etc/timezone \ - && update-locale LANG=C.UTF-8 LC_ALL=C.UTF-8 - -RUN useradd --no-log-init --create-home --shell /bin/bash $IUSER - -USER $IUSER -WORKDIR /home/$IUSER -ENV PATH=/home/$IUSER/.idaes/bin:/home/$IUSER/miniconda3/bin:$PATH:/home/$IUSER/.local/bin - -RUN echo "\n___ INSTALL CONDA ___\n" \ - && curl -sSL https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh \ - -o /tmp/miniconda.sh \ - && bash /tmp/miniconda.sh -bf \ - && rm -rf /tmp/miniconda.sh - -RUN echo "\n___ CREATE BASE CONDA ENV ___\n" \ - && conda install -y python=3 \ - && conda update conda \ - && conda clean --all --yes diff --git a/docker/ubuntu-conda/README-build.md b/docker/ubuntu-conda/README-build.md deleted file mode 100644 index 8ec7ce7271..0000000000 --- a/docker/ubuntu-conda/README-build.md +++ /dev/null @@ -1,10 +0,0 @@ -# Instructions for building the Dockerfile - -Last updated: 5/15/2020 - -Current tag (version) is 2.1 - -Commands: - - docker build . -t idaes/ubuntu18-conda:2.1 - docker push idaes/ubuntu18-conda:2.1 diff --git a/docker/ubuntu-conda/README.md b/docker/ubuntu-conda/README.md deleted file mode 100644 index e8b49626ec..0000000000 --- a/docker/ubuntu-conda/README.md +++ /dev/null @@ -1,12 +0,0 @@ -[![docker pulls](https://img.shields.io/docker/pulls/idaes/ubuntu18-conda.svg)](https://hub.docker.com/r/idaes/ubuntu18-conda/) [![docker stars](https://img.shields.io/docker/stars/idaes/ubuntu18-conda.svg)](https://hub.docker.com/r/idaes/ubuntu18-conda/) [![image metadata](https://images.microbadger.com/badges/image/idaes/ubuntu18-conda.svg)](https://microbadger.com/images/idaes/ubuntu18-conda "idaes/ubuntu18-conda image metadata") - -## About - -This directory is for an Ubuntu 18.04 container, used for -building and testing the code on CircleCI. - -## More help - -Please visit the documentation site for help using and contributing to this image and others. - -* [IDAES PSE on ReadTheDocs](https://idaes-pse.readthedocs.io/en/stable/) 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/developer/devsw.rst b/docs/reference_guides/developer/devsw.rst index ba4413b49c..b364c3f283 100644 --- a/docs/reference_guides/developer/devsw.rst +++ b/docs/reference_guides/developer/devsw.rst @@ -145,18 +145,18 @@ Once you have the repo cloned, you can change into that directory (by default, i will be called "idaes-dev" like the repo) and install the Python packages. But before you do that, you need to get the Python package manager fully up and -running. We use a Python packaging system called Conda_. -Below are instructions for installing a minimal version of Conda, called Miniconda_. +running. We use a Python packaging system called Miniforge_. +Below are the instructions for installing the package manager -- which is a community driven and minimal version of Conda. The full version installs a large number of scientific analysis and visualization libraries that are not required by the IDAES framework. .. _Conda: https://conda.io/ -.. _Miniconda: https://conda.io/en/latest/miniconda.html +.. _Miniforge: https://conda-forge.org/miniforge/ .. code-block:: sh - wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh - bash Miniconda3-latest-Linux-x86_64.sh + wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh + bash Miniforge3-Linux-x86_64.sh Create and activate a conda environment (along with its own copy of ``pip``) for the new IDAES installation **(you will need to** ``conda activate idaes`` diff --git a/docs/reference_guides/model_libraries/generic/property_models/ceos.rst b/docs/reference_guides/model_libraries/generic/property_models/ceos.rst index 697a500895..d15a75e4bf 100644 --- a/docs/reference_guides/model_libraries/generic/property_models/ceos.rst +++ b/docs/reference_guides/model_libraries/generic/property_models/ceos.rst @@ -1,6 +1,9 @@ Cubic Equations of State ======================== +.. deprecated:: 2.7 + Use :class:`idaes.models.properties.modular_properties.eos.ceos` in the Modular Property Framework instead. + This property package implements a general form of a cubic equation of state which can be used for most cubic-type equations of state. This package supports phase equilibrium calculations with a smooth phase transition formulation that makes it amenable for equation oriented optimization. The following equations of state are currently supported: * Peng-Robinson 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/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/docs/tutorials/advanced_install/index.rst b/docs/tutorials/advanced_install/index.rst index c6723d8319..b71a71fc8b 100644 --- a/docs/tutorials/advanced_install/index.rst +++ b/docs/tutorials/advanced_install/index.rst @@ -71,12 +71,12 @@ Create the Python Environment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Once you have the repo cloned, you can change into that directory (by default, it will be called "idaes-pse" like the repo) and install the Python packages. -But before you do that, you need to get the Python package manager fully up and running. We use a Python packaging system called Conda_ and we specifically use its minimal version Miniconda_. If you do not already have Conda, please follow the installation instructions for your operating system in :ref:`getting started`. +But before you do that, you need to get the Python package manager fully up and running. We use a Python packaging system called Conda_ and we specifically use a community driven minimal version Miniforge_. If you do not already have Miniforge, please follow the installation instructions for your operating system in :ref:`getting started`. .. _Conda: https://conda.io/ -.. _Miniconda: https://conda.io/en/latest/miniconda.html +.. _Miniforge: https://conda-forge.org/miniforge/ -After Miniconda is installed, we recommend creating a separate conda environment for IDAES. If you are unfamiliar with environments, a good starting guide is `here `__. Create and activate a conda environment for the new IDAES installation with the following commands (we officially support python |python-min| through |python-max|, with |python-default| as a default): +After Miniforge is installed, we recommend creating a separate conda environment for IDAES. If you are unfamiliar with environments, a good starting guide is `here `__. Create and activate a conda environment for the new IDAES installation with the following commands (we officially support python |python-min| through |python-max|, with |python-default| as a default): .. code-block:: sh diff --git a/docs/tutorials/getting_started/index.rst b/docs/tutorials/getting_started/index.rst index 3d6ca4cb10..43c6b6db38 100644 --- a/docs/tutorials/getting_started/index.rst +++ b/docs/tutorials/getting_started/index.rst @@ -8,7 +8,7 @@ Getting Started Installation ------------ To install the IDAES PSE framework, follow the set of instructions below that are appropriate for -your needs. The OS specific instructions provide optional steps for installing Miniconda, which can be +your needs. The OS specific instructions provide optional steps for installing Miniforge, which can be skipped. If you are an IDAES developer or expect to change IDAES code, we recommend following the :ref:`advanced user installation`. Please contact `idaes-support@idaes.org `_, if you have difficulty installing 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 diff --git a/docs/tutorials/getting_started/linux.rst b/docs/tutorials/getting_started/linux.rst index 49c88006b3..a09dcaff81 100644 --- a/docs/tutorials/getting_started/linux.rst +++ b/docs/tutorials/getting_started/linux.rst @@ -29,11 +29,14 @@ To get IDAES fully set up on your machine, we'll go through the steps to get ida Install Prerequisites ^^^^^^^^^^^^^^^^^^^^^ -**Install Miniconda** +**Install Miniforge** -1. Download `Miniconda `_ +1. Download `Miniforge `_ 2. Open a terminal window & run the downloaded script. +** If you are running Arm64 or Power8/9 architecture, make sure you pull the applicable installer from the link below ** +https://github.com/conda-forge/miniforge/releases + **Install Dependencies** 1. The IPOPT solver depends on the GNU FORTRAN, GOMP, Blas, and Lapack libraries. diff --git a/docs/tutorials/getting_started/mac_osx.rst b/docs/tutorials/getting_started/mac_osx.rst index 0453e23e98..0b2d64b185 100644 --- a/docs/tutorials/getting_started/mac_osx.rst +++ b/docs/tutorials/getting_started/mac_osx.rst @@ -34,11 +34,11 @@ To get IDAES fully set up on your machine, we'll go through the steps to get ida Install Prerequisites ^^^^^^^^^^^^^^^^^^^^^ -**Install Miniconda** - -1. Download `Miniconda `_ +**Install Miniforge** +1. Download `Miniforge `_ 2. Open a terminal window & run the downloaded script. + Install IDAES-PSE ^^^^^^^^^^^^^^^^^^ diff --git a/docs/tutorials/getting_started/windows.rst b/docs/tutorials/getting_started/windows.rst index 37510ac8de..ef1ed80f44 100644 --- a/docs/tutorials/getting_started/windows.rst +++ b/docs/tutorials/getting_started/windows.rst @@ -29,10 +29,10 @@ To get IDAES fully set up on your machine, we'll go through the steps to get ida Install Prerequisites ^^^^^^^^^^^^^^^^^^^^^ -**Install Miniconda** +**Install Miniforge** -1. Download & install `Miniconda `_. -2. Install anaconda from the downloaded & open the Anaconda Prompt (Start -> "Anaconda Prompt"). +1. Download & install `Miniforge `_. +2. Install miniforge from the downloaded executable Install IDAES-PSE ^^^^^^^^^^^^^^^^^^ 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 = { 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() diff --git a/idaes/models/properties/cubic_eos/cubic_prop_pack.py b/idaes/models/properties/cubic_eos/cubic_prop_pack.py index 42ca44340c..dfb914f839 100644 --- a/idaes/models/properties/cubic_eos/cubic_prop_pack.py +++ b/idaes/models/properties/cubic_eos/cubic_prop_pack.py @@ -50,6 +50,7 @@ ) from pyomo.common.config import ConfigDict, ConfigValue, In from pyomo.contrib.incidence_analysis import solve_strongly_connected_components +from pyomo.common.deprecation import deprecated # Import IDAES cores from idaes.core import ( @@ -97,6 +98,12 @@ _log = idaeslog.getLogger(__name__) +@deprecated( + msg="The standalone cubic property package has been deprecated in favor of the " + "cubic equation of state for the modular property framework. This class will be " + "removed in the May 2025 release.", + version="2.7.0", +) @declare_process_block_class("CubicParameterBlock") class CubicParameterData(PhysicalParameterBlock): """ @@ -222,6 +229,12 @@ def define_metadata(cls, obj): ) +@deprecated( + msg="The standalone cubic property package has been deprecated in favor of the " + "cubic equation of state for the modular property framework. This class will be " + "removed in the May 2025 release.", + version="2.7.0", +) class CubicEoSInitializer(InitializerBase): """ Initializer for CubicEoS property packages. diff --git a/idaes/models/properties/cubic_eos/tests/test_BT_example.py b/idaes/models/properties/cubic_eos/tests/test_BT_example.py index b9b3a43ff1..9a452062e3 100644 --- a/idaes/models/properties/cubic_eos/tests/test_BT_example.py +++ b/idaes/models/properties/cubic_eos/tests/test_BT_example.py @@ -26,6 +26,7 @@ ) from pyomo.util.check_units import assert_units_consistent +import idaes.logger as idaeslog from idaes.models.properties.tests.test_harness import PropertyTestHarness from idaes.core.solvers import get_solver @@ -76,12 +77,13 @@ def configure(self): @pytest.mark.skipif(not prop_available, reason="Cubic root finder not available") class TestBTExample(object): @pytest.mark.component - def test_units(self): + def test_units(self, caplog): m = ConcreteModel() - m.fs = FlowsheetBlock(dynamic=False) - m.fs.props = BT_PR.BTParameterBlock(valid_phase=("Vap", "Liq")) + with caplog.at_level(idaeslog.WARNING): + m.fs.props = BT_PR.BTParameterBlock(valid_phase=("Vap", "Liq")) + assert "May 2025 release." in caplog.text m.fs.state = m.fs.props.build_state_block([0], defined_state=True) diff --git a/idaes/models/properties/cubic_eos/tests/test_cubic_prop_pack.py b/idaes/models/properties/cubic_eos/tests/test_cubic_prop_pack.py index 6bd4a945e0..c641acdbb4 100644 --- a/idaes/models/properties/cubic_eos/tests/test_cubic_prop_pack.py +++ b/idaes/models/properties/cubic_eos/tests/test_cubic_prop_pack.py @@ -24,6 +24,7 @@ ) from idaes.core import FlowsheetBlock, Component +import idaes.logger as idaeslog from idaes.models.properties.cubic_eos.cubic_prop_pack import ( CubicParameterBlock, CubicStateBlock, @@ -51,12 +52,13 @@ class TestParameterBlock(object): not cubic_roots_available(), reason="Cubic functions not available" ) @pytest.mark.unit - def test_build_default(self): + def test_build_default(self, caplog): m = ConcreteModel() m.fs = FlowsheetBlock(dynamic=False) - - m.fs.params = CubicParameterBlock() + with caplog.at_level(idaeslog.WARNING): + m.fs.params = CubicParameterBlock() + assert "May 2025 release." in caplog.text assert m.fs.params.state_block_class is CubicStateBlock assert m.fs.params.config.valid_phase == ("Vap", "Liq") 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) 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 + ) 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) diff --git a/requirements-dev.txt b/requirements-dev.txt index 69c92e612c..0026f61160 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,12 +1,11 @@ ---index-url https://pypi.python.org/simple/ - # Developer extra packages ### docs -alabaster>=0.7.7 # Newer sphinx needed for proper type hint support in docstrings sphinx>=3.0.0 sphinxcontrib-napoleon>=0.5.0 +# sphinx-argparse 0.4.0 is the last version to support Python 3.9 +# see https://sphinx-argparse.readthedocs.io/en/latest/changelog.html#id3 sphinx-argparse==0.4.0 sphinx-book-theme<=1.1.2,>=1.0.0 sphinx-copybutton==0.5.2 @@ -19,15 +18,11 @@ pytest-cov # @lbianchi-lbl: both pylint and astroid should be tightly pinned; see .pylint/idaes_transform.py for more info pylint==3.0.3 astroid==3.0.3 -flake8 black==24.3.0 # pre-commit install, manage, and run pre-commit hooks pre-commit ### other/misc -jsonschema -jupyter_contrib_nbextensions -snowballstemmer==1.2.1 addheader>=0.2.2 # this will install IDAES in editable mode using the dependencies defined under the `extras_require` tags defined in `setup.py`