diff --git a/idaes_examples/notebooks/docs/unit_models/operations/eg_h2o_ideal.py b/idaes_examples/notebooks/docs/unit_models/operations/eg_h2o_ideal.py index 0bf2a5ed..40607b83 100644 --- a/idaes_examples/notebooks/docs/unit_models/operations/eg_h2o_ideal.py +++ b/idaes_examples/notebooks/docs/unit_models/operations/eg_h2o_ideal.py @@ -36,110 +36,136 @@ # --------------------------------------------------------------------- -# Configuration dictionary for an ideal ethylene oxide, water, -# sulfuric acid, and ethylene glycol system +# Configuration dictionary for an ideal ethylene glycol and water system # Data Sources: # [1] The Properties of Gases and Liquids (1987) # 4th edition, Chemical Engineering Series - Robert C. Reid # [2] Perry's Chemical Engineers' Handbook 7th Ed. # [3] NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ -# Retrieved 23rd September, 2021 +# Retrieved 18th March, 2024 config_dict = { # Specifying components "components": { - 'water': - {"type": Component, - "elemental_composition": {"H": 2, "O": 1}, - "dens_mol_liq_comp": Perrys, - "enth_mol_liq_comp": Perrys, - "enth_mol_ig_comp": RPP4, - "pressure_sat_comp": RPP4, - "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, - "parameter_data": { - "mw": (18.015E-3, pyunits.kg/pyunits.mol), # [1] - "pressure_crit": (221.2e5, pyunits.Pa), # [1] - "temperature_crit": (647.3, pyunits.K), # [1] - "dens_mol_liq_comp_coeff": { - 'eqn_type': 2, - '1': (-13.851, pyunits.kmol/pyunits.m**3), # [2]pg. 2-98 - '2': (0.64038, pyunits.kmol/pyunits.m**3/pyunits.K), - '3': (-0.00191, pyunits.kmol/pyunits.m**3/pyunits.K**2), - '4': (1.8211E-6, pyunits.kmol/pyunits.m**3/pyunits.K**3)}, - "cp_mol_ig_comp_coeff": { - 'A': (3.194E1, pyunits.J/pyunits.mol/pyunits.K), # [1] - 'B': (1.436E-3, pyunits.J/pyunits.mol/pyunits.K**2), - 'C': (2.432E-5, pyunits.J/pyunits.mol/pyunits.K**3), - 'D': (-1.176E-8, pyunits.J/pyunits.mol/pyunits.K**4)}, - "cp_mol_liq_comp_coeff": { - '1': (2.7637E2, pyunits.J/pyunits.kmol/pyunits.K), # [2] - '2': (-2.0901, pyunits.J/pyunits.kmol/pyunits.K**2), - '3': (8.125E-3, pyunits.J/pyunits.kmol/pyunits.K**3), - '4': (-1.4116E-5, pyunits.J/pyunits.kmol/pyunits.K**4), - '5': (9.3701E-9, pyunits.J/pyunits.kmol/pyunits.K**5)}, - "enth_mol_form_liq_comp_ref": ( - -285.83e3, pyunits.J/pyunits.mol), # [3] - "enth_mol_form_vap_comp_ref": ( - -241.836e3, pyunits.J/pyunits.mol), # [3] - "pressure_sat_comp_coeff": {'A': (-7.76451, None), # [1] - 'B': (1.45838, None), - 'C': (-2.77580, None), - 'D': (-1.23303, None)}}}, - 'ethylene_glycol': - {"type": Component, - "elemental_composition": {"C": 2, "H": 6, "O": 2}, - "dens_mol_liq_comp": Perrys, - "enth_mol_liq_comp": Perrys, - "enth_mol_ig_comp": RPP4, - "pressure_sat_comp": RPP4, - "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, - "parameter_data": { - "mw": (62.069E-3, pyunits.kg/pyunits.mol), # [1] - "pressure_crit": (77e5, pyunits.Pa), # [1] - "temperature_crit": (645, pyunits.K), # [1] - "dens_mol_liq_comp_coeff": { - 'eqn_type': 1, - '1': (1.315, pyunits.kmol*pyunits.m**-3), # [2] pg. 2-98 - '2': (0.25125, None), - '3': (720, pyunits.K), - '4': (0.21868, None)}, - "cp_mol_ig_comp_coeff": { - 'A': (3.570E1, pyunits.J/pyunits.mol/pyunits.K), # [1] - 'B': (2.483E-1, pyunits.J/pyunits.mol/pyunits.K**2), - 'C': (-1.497E-4, pyunits.J/pyunits.mol/pyunits.K**3), - 'D': (3.010E-8, pyunits.J/pyunits.mol/pyunits.K**4)}, - "cp_mol_liq_comp_coeff": { - '1': (3.5540E1, pyunits.J/pyunits.kmol/pyunits.K), # [2] - '2': (4.3678E-1, pyunits.J/pyunits.kmol/pyunits.K**2), - '3': (-1.8486E-4, pyunits.J/pyunits.kmol/pyunits.K**3), - '4': (0, pyunits.J/pyunits.kmol/pyunits.K**4), - '5': (0, pyunits.J/pyunits.kmol/pyunits.K**5)}, - "enth_mol_form_liq_comp_ref": ( - -455.24e3, pyunits.J/pyunits.mol), # [3] - "enth_mol_form_vap_comp_ref": ( - -389.37e3, pyunits.J/pyunits.mol), # [3] - "pressure_sat_comp_coeff": {'A': (13.6299, None), # [1] - 'B': (6022.18, None), - 'C': (-28.25, None), - 'D': (0, None)}}}}, - + "water": { + "type": Component, + "elemental_composition": {"H": 2, "O": 1}, + "dens_mol_liq_comp": Perrys, + "enth_mol_liq_comp": Perrys, + "enth_mol_ig_comp": RPP4, + "pressure_sat_comp": RPP4, + "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, + "parameter_data": { + "mw": (18.015e-3, pyunits.kg / pyunits.mol), # [1] pg. 667 + "pressure_crit": (221.2e5, pyunits.Pa), # [1] pg. 667 + "temperature_crit": (647.3, pyunits.K), # [1] pg. 667 + "dens_mol_liq_comp_coeff": { # [2] pg. 2-98 + "eqn_type": 1, + "1": (5.459, pyunits.kmol * pyunits.m**-3), + "2": (0.30542, None), + "3": (647.13, pyunits.K), + "4": (0.081, None), + }, + "cp_mol_ig_comp_coeff": { # [1] pg. 668 + "A": (3.224e1, pyunits.J / pyunits.mol / pyunits.K), + "B": (1.924e-3, pyunits.J / pyunits.mol / pyunits.K**2), + "C": (1.055e-5, pyunits.J / pyunits.mol / pyunits.K**3), + "D": (-3.596e-9, pyunits.J / pyunits.mol / pyunits.K**4), + }, + "cp_mol_liq_comp_coeff": { # [2] pg. 2-174 + "1": (2.7637e5, pyunits.J / pyunits.kmol / pyunits.K), + "2": (-2.0901e3, pyunits.J / pyunits.kmol / pyunits.K**2), + "3": (8.1250, pyunits.J / pyunits.kmol / pyunits.K**3), + "4": (-1.4116e-2, pyunits.J / pyunits.kmol / pyunits.K**4), + "5": (9.3701e-6, pyunits.J / pyunits.kmol / pyunits.K**5), + }, + "enth_mol_form_liq_comp_ref": ( + -285.830e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + "enth_mol_form_vap_comp_ref": ( + -241.826e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + "pressure_sat_comp_coeff": { + "A": (-7.76451, None), # [1] pg. 669 + "B": (1.45838, None), + "C": (-2.77580, None), + "D": (-1.23303, None), + }, + }, + }, + "ethylene_glycol": { + "type": Component, + "elemental_composition": {"C": 2, "H": 6, "O": 2}, + "dens_mol_liq_comp": Perrys, + "enth_mol_liq_comp": Perrys, + "enth_mol_ig_comp": RPP4, + "pressure_sat_comp": RPP4, + "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, + "parameter_data": { + "mw": (62.069e-3, pyunits.kg / pyunits.mol), # [1] pg. 676 + "pressure_crit": (77e5, pyunits.Pa), # [1] pg. 676 + "temperature_crit": (645, pyunits.K), # [1] pg. 676 + "dens_mol_liq_comp_coeff": { # [2] pg. 2-95 + "eqn_type": 1, + "1": (1.3151, pyunits.kmol * pyunits.m**-3), + "2": (0.25125, None), + "3": (719.7, pyunits.K), + "4": (0.2187, None), + }, + "cp_mol_ig_comp_coeff": { # [1] pg. 677 + "A": (3.570e1, pyunits.J / pyunits.mol / pyunits.K), + "B": (2.483e-1, pyunits.J / pyunits.mol / pyunits.K**2), + "C": (-1.497e-4, pyunits.J / pyunits.mol / pyunits.K**3), + "D": (3.010e-8, pyunits.J / pyunits.mol / pyunits.K**4), + }, + "cp_mol_liq_comp_coeff": { # [2] pg. 2-171 + "1": (3.5540e4, pyunits.J / pyunits.kmol / pyunits.K), + "2": (4.3678e2, pyunits.J / pyunits.kmol / pyunits.K**2), + "3": (-1.8486e-1, pyunits.J / pyunits.kmol / pyunits.K**3), + "4": (0, pyunits.J / pyunits.kmol / pyunits.K**4), + "5": (0, pyunits.J / pyunits.kmol / pyunits.K**5), + }, + "enth_mol_form_liq_comp_ref": ( + -460.0e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + "enth_mol_form_vap_comp_ref": ( + -394.4e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + # [1] pg. 678 pressure sat coef values for alternative equation form + # ln Pvp = A - B/(T + C) with A = 13.6299, B = 6022.18, C = -28.25 + # reformulated for generic property supported form + # ln Pvp = [(1 - x)^-1 * (A*x + B*x^1.5 + C*x^3 + D*x^6)] * Pc where x = 1 - T/Tc + "pressure_sat_comp_coeff": { + "A": (-16.4022, None), + "B": (10.0100, None), + "C": (-6.5216, None), + "D": (-11.1182, None), + }, + }, + }, + }, # Specifying phases - "phases": {'Liq': {"type": LiquidPhase, - "equation_of_state": Ideal}}, - + "phases": {"Liq": {"type": LiquidPhase, "equation_of_state": Ideal}}, # Set base units of measurement - "base_units": {"time": pyunits.s, - "length": pyunits.m, - "mass": pyunits.kg, - "amount": pyunits.mol, - "temperature": pyunits.K}, - + "base_units": { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + }, # Specifying state definition "state_definition": FpcTP, - "state_bounds": {"flow_mol_phase_comp": (0, 100, 1000, - pyunits.mol/pyunits.s), - "temperature": (273.15, 298.15, 450, pyunits.K), - "pressure": (1e3, 1e5, 1e6, pyunits.Pa)}, + "state_bounds": { + "flow_mol_phase_comp": (0, 100, 1000, pyunits.mol / pyunits.s), + "temperature": (273.15, 298.15, 450, pyunits.K), + "pressure": (1e3, 1e5, 1e6, pyunits.Pa), + }, "pressure_ref": (1e5, pyunits.Pa), - "temperature_ref": (298.15, pyunits.K)} + "temperature_ref": (298.15, pyunits.K), +} diff --git a/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit.ipynb b/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit.ipynb index 922f495f..12aac7c6 100644 --- a/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit.ipynb @@ -583,18 +583,18 @@ "\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1426, rel=1e-3)\n", + ") == pytest.approx(0.14258566, rel=1e-5)\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.0002667, rel=1e-3)\n", + ") == pytest.approx(0.000266748768, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1974, rel=1e-3)\n", + ") == pytest.approx(0.19741534, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.6597, rel=1e-3)\n", - "assert value(m.fs.separation_factor) == pytest.approx(1038, rel=1e-3)\n", - "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5813, rel=1e-3)" + ") == pytest.approx(0.65973425, rel=1e-5)\n", + "assert value(m.fs.separation_factor) == pytest.approx(1037.6188, rel=1e-5)\n", + "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5812.7111, rel=1e-5)" ] }, { @@ -671,18 +671,18 @@ "\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1426, rel=1e-3)\n", + ") == pytest.approx(0.14258566, rel=1e-5)\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.0002667, rel=1e-3)\n", + ") == pytest.approx(0.000266748768, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.6998, rel=1e-3)\n", + ") == pytest.approx(0.69981938, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.1573, rel=1e-3)\n", - "assert value(m.fs.separation_factor) == pytest.approx(100.0, rel=1e-3)\n", - "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5813, rel=1e-3)" + ") == pytest.approx(0.15733020, rel=1e-5)\n", + "assert value(m.fs.separation_factor) == pytest.approx(100.000067, rel=1e-5)\n", + "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5812.7111, rel=1e-5)" ] }, { @@ -719,9 +719,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_doc.ipynb b/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_doc.ipynb index 0273e79d..e3cbb4d7 100644 --- a/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_doc.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_doc.ipynb @@ -1,1208 +1,727 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "tags": [ - "header", - "hide-cell" - ] - }, - "outputs": [], - "source": [ - "###############################################################################\n", - "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", - "# Framework (IDAES IP) was produced under the DOE Institute for the\n", - "# Design of Advanced Energy Systems (IDAES).\n", - "#\n", - "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", - "# University of California, through Lawrence Berkeley National Laboratory,\n", - "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", - "# University, West Virginia University Research Corporation, et al.\n", - "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", - "# for full copyright and license information.\n", - "###############################################################################" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# IDAES Skeleton Unit Model\n", - "Maintainer: Brandon Paul \n", - "Author: Brandon Paul \n", - "Updated: 2023-06-01 \n", - "\n", - "This notebook demonstrates usage of the IDAES Skeleton Unit Model, which provides a generic \"bare bones\" unit for user-defined models and custom variable and constraint sets. To allow maximum versatility, this unit may be defined as a surrogate model or a custom equation-oriented model. Users must add ports and variables that match connected models, and this is facilitated through a provided method to add port-variable sets.\n", - "\n", - "For users who wish to train surrogates with IDAES tools and insert obtained models into a flowsheet, see more detailed information on [IDAES Surrogate Tools](https://idaes-pse.readthedocs.io/en/stable/explanations/modeling_extensions/surrogate/index.html)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1. Motivation\n", - "\n", - "In many cases, a specific application requires a unique unit operation that does not exist in the IDAES repository. Custom user models may source from external scripts, import surrogate equations or use first-principles calculations. However, IDAES flowsheets adhere to a standardized modeling hierarchy and simple Pyomo models do not always follow these conventions. Additionally, simple flowsheet submodels often require integration with other IDAES unit models which requires consistency between corresponding port variables, stream properties and physical unit sets, as well as proper usage of `ControlVolume` blocks.\n", - "\n", - "The IDAES `SkeletonUnitModel` allows custom creation of user models blocks that do not require `ControlVolume` blocks, and enabling connection with standard IDAES unit models that do contain `ControlVolume` blocks. To motivate the usefulness and versatility of this tool, we will consider a simple pervaporation unit. The custom model does not require rigorous thermodynamic calculations contained in adjacent unit models, and using a Skeleton model allows definition of only required variables and constraints. The new block does require state variable connections for the inlet and outlet streams. We will demonstrate this scenario below to highlight the usage and benefits of the Skeleton model." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 2. Example - Pervaporation\n", - "\n", - "Pervaporation is a low-energy separation process, and is particularly advantageous over distillation for azeotropic solutions or aqueous mixtures of heavy alcohols. Ethylene glycol is more environmentally friendly than typical chloride- and bromide-based dessicants, and is a common choice for commercial recovery of water from flue gas via liquid spray columns. Due to ethylene glycol's high boiling point, diffusion-based water recovery is economically favorable compared to distillation-based processes. The following example and flux correlation are taken from the literature source below:\n", - "\n", - "Jennifer Runhong Du, Amit Chakma, X. Feng, Dehydration of ethylene glycol by pervaporation using poly(N,N-dimethylaminoethyl methacrylate)/polysulfone composite membranes, Separation and Purification Technology, Volume 64, Issue 1, 2008, Pages 63-70, ISSN 1383-5866, https://doi.org/10.1016/j.seppur.2008.08.004.\n", - "\n", - "The process is adapted from the literature, utilizing an inlet aqueous glycol feed circulated through a feed tank-membrane-feed tank recycle loop while permeate is continuously extracted by the membrane. To demonstrate the usefulness of the Skeleton model, we will model this system as a Mixer and custom Pervaporation unit per the diagram below and define the flux as an empirical custom mass balance term rather than requiring rigorous diffusion calculations. We will also circumvent the need for a vapor phase and VLE calculations by manually calculating the duty to condense and collect permeate vapor, and use correlations for steady-state fluxes to avoid a recycle requiring tear calculations.\n", - "\n", - "![](pervaporation_process.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2.1 Pyomo and IDAES Imports\n", - "We will begin with relevant imports. We will need basic Pyomo and IDAES components:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "import pytest\n", - "from pyomo.environ import (\n", - " check_optimal_termination,\n", - " ConcreteModel,\n", - " Constraint,\n", - " Expression,\n", - " Objective,\n", - " maximize,\n", - " Var,\n", - " Set,\n", - " TransformationFactory,\n", - " value,\n", - " exp,\n", - " units as pyunits,\n", - ")\n", - "from pyomo.network import Arc\n", - "from idaes.core import FlowsheetBlock\n", - "from idaes.models.unit_models import Feed, SkeletonUnitModel, Mixer, Product\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.util.initialization import propagate_state\n", - "from idaes.core.solvers import get_solver\n", - "from pyomo.util.check_units import assert_units_consistent\n", - "\n", - "# import thermophysical properties\n", - "import eg_h2o_ideal as thermo_props\n", - "from idaes.models.properties.modular_properties import GenericParameterBlock\n", - "from idaes.core.util.constants import Constants" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2.2 Build Flowsheet\n", - "\n", - "We will build a simple model manually defining state variables relations entering and exiting the pervaporation unit. As shown below, we may define our pre-separation mixer as usual:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "# build the flowsheet\n", - "m = ConcreteModel()\n", - "m.fs = FlowsheetBlock(dynamic=False)\n", - "\n", - "m.fs.thermo_params = GenericParameterBlock(**thermo_props.config_dict)\n", - "\n", - "m.fs.WATER = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.GLYCOL = Feed(property_package=m.fs.thermo_params)\n", - "\n", - "m.fs.M101 = Mixer(\n", - " property_package=m.fs.thermo_params, inlet_list=[\"water_feed\", \"glycol_feed\"]\n", - ")\n", - "\n", - "m.fs.RETENTATE = Product(property_package=m.fs.thermo_params)\n", - "m.fs.PERMEATE = Product(property_package=m.fs.thermo_params)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2.2 Defining Skeleton Model and Connections\n", - "\n", - "Now that our flowsheet exists, we can manually define variables, units, constraints and ports for our custom pervaporation unit model. By using a Skeleton model, we avoid rigorous mass and energy balances and phase equilibrium which impact model tractability. Instead, we define state variable relations as below - note that we include the fluxes as outlet flow terms. In this model, the variables specify an `FpcTP` system where molar flow of each component, temperature and pressure are selected as state variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "# define Skeleton model for pervaporation unit\n", - "m.fs.pervap = SkeletonUnitModel(dynamic=False)\n", - "m.fs.pervap.comp_list = Set(initialize=[\"water\", \"ethylene_glycol\"])\n", - "m.fs.pervap.phase_list = Set(initialize=[\"Liq\"])\n", - "\n", - "# input vars for skeleton\n", - "# m.fs.time is a pre-initialized Set belonging to the FlowsheetBlock; for dynamic=False, time=[0]\n", - "m.fs.pervap.flow_in = Var(\n", - " m.fs.time,\n", - " m.fs.pervap.phase_list,\n", - " m.fs.pervap.comp_list,\n", - " initialize=1.0,\n", - " units=pyunits.mol / pyunits.s,\n", - ")\n", - "m.fs.pervap.temperature_in = Var(m.fs.time, initialize=298.15, units=pyunits.K)\n", - "m.fs.pervap.pressure_in = Var(m.fs.time, initialize=101e3, units=pyunits.Pa)\n", - "\n", - "# output vars for skeleton\n", - "m.fs.pervap.perm_flow = Var(\n", - " m.fs.time,\n", - " m.fs.pervap.phase_list,\n", - " m.fs.pervap.comp_list,\n", - " initialize=1.0,\n", - " units=pyunits.mol / pyunits.s,\n", - ")\n", - "m.fs.pervap.ret_flow = Var(\n", - " m.fs.time,\n", - " m.fs.pervap.phase_list,\n", - " m.fs.pervap.comp_list,\n", - " initialize=1.0,\n", - " units=pyunits.mol / pyunits.s,\n", - ")\n", - "m.fs.pervap.temperature_out = Var(m.fs.time, initialize=298.15, units=pyunits.K)\n", - "m.fs.pervap.pressure_out = Var(m.fs.time, initialize=101e3, units=pyunits.Pa)\n", - "m.fs.pervap.vacuum = Var(m.fs.time, initialize=1.3e3, units=pyunits.Pa)\n", - "\n", - "# dictionaries relating state properties to custom variables\n", - "inlet_dict = {\n", - " \"flow_mol_phase_comp\": m.fs.pervap.flow_in,\n", - " \"temperature\": m.fs.pervap.temperature_in,\n", - " \"pressure\": m.fs.pervap.pressure_in,\n", - "}\n", - "retentate_dict = {\n", - " \"flow_mol_phase_comp\": m.fs.pervap.ret_flow,\n", - " \"temperature\": m.fs.pervap.temperature_out,\n", - " \"pressure\": m.fs.pervap.pressure_out,\n", - "}\n", - "permeate_dict = {\n", - " \"flow_mol_phase_comp\": m.fs.pervap.perm_flow,\n", - " \"temperature\": m.fs.pervap.temperature_out,\n", - " \"pressure\": m.fs.pervap.vacuum,\n", - "}\n", - "\n", - "m.fs.pervap.add_ports(name=\"inlet\", member_dict=inlet_dict)\n", - "m.fs.pervap.add_ports(name=\"retentate\", member_dict=retentate_dict)\n", - "m.fs.pervap.add_ports(name=\"permeate\", member_dict=permeate_dict)\n", - "\n", - "# internal vars for skeleton\n", - "energy_activation_dict = {\n", - " (0, \"Liq\", \"water\"): 51e3,\n", - " (0, \"Liq\", \"ethylene_glycol\"): 53e3,\n", - "}\n", - "m.fs.pervap.energy_activation = Var(\n", - " m.fs.time,\n", - " m.fs.pervap.phase_list,\n", - " m.fs.pervap.comp_list,\n", - " initialize=energy_activation_dict,\n", - " units=pyunits.J / pyunits.mol,\n", - ")\n", - "m.fs.pervap.energy_activation.fix()\n", - "\n", - "permeance_dict = {\n", - " (0, \"Liq\", \"water\"): 5611320,\n", - " (0, \"Liq\", \"ethylene_glycol\"): 22358.88,\n", - "} # calculated from literature data\n", - "m.fs.pervap.permeance = Var(\n", - " m.fs.time,\n", - " m.fs.pervap.phase_list,\n", - " m.fs.pervap.comp_list,\n", - " initialize=permeance_dict,\n", - " units=pyunits.mol / pyunits.s / pyunits.m**2,\n", - ")\n", - "m.fs.pervap.permeance.fix()\n", - "\n", - "m.fs.pervap.area = Var(m.fs.time, initialize=6, units=pyunits.m**2)\n", - "m.fs.pervap.area.fix()\n", - "\n", - "latent_heat_dict = {\n", - " (0, \"Liq\", \"water\"): 40.660e3,\n", - " (0, \"Liq\", \"ethylene_glycol\"): 56.9e3,\n", - "}\n", - "m.fs.pervap.latent_heat_of_vaporization = Var(\n", - " m.fs.time,\n", - " m.fs.pervap.phase_list,\n", - " m.fs.pervap.comp_list,\n", - " initialize=latent_heat_dict,\n", - " units=pyunits.J / pyunits.mol,\n", - ")\n", - "m.fs.pervap.latent_heat_of_vaporization.fix()\n", - "m.fs.pervap.heat_duty = Var(\n", - " m.fs.time, initialize=1, units=pyunits.J / pyunits.s\n", - ") # we will calculate this later" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's define our surrogate equations for flux and permeance, and link them to the port variables. Users can use this structure to write custom relations between inlet and outlet streams; for example, here we define the outlet flow of the pervaporation unit as a sum of the inlet flow and calculated recovery fluxes. By defining model constraints in lieu of rigorous mass balances, we add the flux as a custom mass balance term via an empirical correlation and calculate only the condensation duty rather than implementing full energy balance calculations:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "# Surrogate and first principles model equations\n", - "\n", - "# flux equation (gas constant is defined as J/mol-K)\n", - "\n", - "\n", - "def rule_permeate_flux(pervap, t, p, i):\n", - " return pervap.permeate.flow_mol_phase_comp[t, p, i] / pervap.area[t] == (\n", - " pervap.permeance[t, p, i]\n", - " * exp(\n", - " -pervap.energy_activation[t, p, i]\n", - " / (Constants.gas_constant * pervap.inlet.temperature[t])\n", - " )\n", - " )\n", - "\n", - "\n", - "m.fs.pervap.eq_permeate_flux = Constraint(\n", - " m.fs.time, m.fs.pervap.phase_list, m.fs.pervap.comp_list, rule=rule_permeate_flux\n", - ")\n", - "\n", - "# permeate condensation equation\n", - "# heat duty based on condensing all of permeate product vapor\n", - "# avoids the need for a Heater or HeatExchanger unit model\n", - "\n", - "\n", - "def rule_duty(pervap, t):\n", - " return pervap.heat_duty[t] == sum(\n", - " pervap.latent_heat_of_vaporization[t, p, i]\n", - " * pervap.permeate.flow_mol_phase_comp[t, p, i]\n", - " for p in pervap.phase_list\n", - " for i in pervap.comp_list\n", - " )\n", - "\n", - "\n", - "m.fs.pervap.eq_duty = Constraint(m.fs.time, rule=rule_duty)\n", - "\n", - "# flow equation adding total recovery as a custom mass balance term\n", - "def rule_retentate_flow(pervap, t, p, i):\n", - " return pervap.retentate.flow_mol_phase_comp[t, p, i] == (\n", - " pervap.inlet.flow_mol_phase_comp[t, p, i]\n", - " - pervap.permeate.flow_mol_phase_comp[t, p, i]\n", - " )\n", - "\n", - "\n", - "m.fs.pervap.eq_retentate_flow = Constraint(\n", - " m.fs.time, m.fs.pervap.phase_list, m.fs.pervap.comp_list, rule=rule_retentate_flow\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, let's define the Arc connecting our two models (IDAES Mixer and custom Pervaporation) and build the flowsheet network:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.s01 = Arc(source=m.fs.WATER.outlet, destination=m.fs.M101.water_feed)\n", - "m.fs.s02 = Arc(source=m.fs.GLYCOL.outlet, destination=m.fs.M101.glycol_feed)\n", - "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.pervap.inlet)\n", - "m.fs.s04 = Arc(source=m.fs.pervap.permeate, destination=m.fs.PERMEATE.inlet)\n", - "m.fs.s05 = Arc(source=m.fs.pervap.retentate, destination=m.fs.RETENTATE.inlet)\n", - "TransformationFactory(\"network.expand_arcs\").apply_to(m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's see how many degrees of freedom the flowsheet has:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "11\n" - ] - } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2.3 Inlet Specifications\n", - "\n", - "To obtain a square problem with zero degrees of freedom, we specify the inlet water flow, ethylene glycol flow, temperature and pressure for each feed stream, as well as the permeate stream pressure:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.WATER.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(0.34) # mol/s\n", - "m.fs.WATER.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(1e-6) # mol/s\n", - "m.fs.WATER.outlet.temperature.fix(318.15) # K\n", - "m.fs.WATER.outlet.pressure.fix(101.325e3) # Pa\n", - "\n", - "m.fs.GLYCOL.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(1e-6) # mol/s\n", - "m.fs.GLYCOL.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(0.66) # mol/s\n", - "m.fs.GLYCOL.outlet.temperature.fix(318.15) # K\n", - "m.fs.GLYCOL.outlet.pressure.fix(101.325e3) # Pa" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Additionally, we need to pass rules defining the temperature and pressure outlets of the pervaporation unit:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "# Add a constraint to calculate the outlet temperature.\n", - "# Here, assume outlet temperature is the same as inlet temperature for illustration\n", - "# in reality, temperature change from latent heat loss through membrane is negligible\n", - "\n", - "\n", - "def rule_temp_out(pervap, t):\n", - " return pervap.inlet.temperature[t] == pervap.retentate.temperature[t]\n", - "\n", - "\n", - "m.fs.pervap.temperature_out_calculation = Constraint(m.fs.time, rule=rule_temp_out)\n", - "\n", - "# Add a constraint to calculate the retentate pressure\n", - "# Here, assume the retentate pressure is the same as the inlet pressure for illustration\n", - "# in reality, pressure change from mass loss through membrane is negligible\n", - "\n", - "\n", - "def rule_pres_out(pervap, t):\n", - " return pervap.inlet.pressure[t] == pervap.retentate.pressure[t]\n", - "\n", - "\n", - "m.fs.pervap.pressure_out_calculation = Constraint(m.fs.time, rule=rule_pres_out)\n", - "\n", - "# fix permeate vacuum pressure\n", - "m.fs.PERMEATE.inlet.pressure.fix(1.3e3)\n", - "\n", - "assert degrees_of_freedom(m) == 0" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2.4 Custom Initialization\n", - "In addition to allowing custom variable and constraint definitions, the Skeleton model enables implementation of a custom initialization scheme. Complex unit operations may present unique tractability issues, and users have precise control over piecewise unit model solving." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "header", + "hide-cell" + ] + }, + "outputs": [], + "source": [ + "###############################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "###############################################################################" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.WATER.properties: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# IDAES Skeleton Unit Model\n", + "Maintainer: Brandon Paul \n", + "Author: Brandon Paul \n", + "Updated: 2023-06-01 \n", + "\n", + "This notebook demonstrates usage of the IDAES Skeleton Unit Model, which provides a generic \"bare bones\" unit for user-defined models and custom variable and constraint sets. To allow maximum versatility, this unit may be defined as a surrogate model or a custom equation-oriented model. Users must add ports and variables that match connected models, and this is facilitated through a provided method to add port-variable sets.\n", + "\n", + "For users who wish to train surrogates with IDAES tools and insert obtained models into a flowsheet, see more detailed information on [IDAES Surrogate Tools](https://idaes-pse.readthedocs.io/en/stable/explanations/modeling_extensions/surrogate/index.html)." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.WATER.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1. Motivation\n", + "\n", + "In many cases, a specific application requires a unique unit operation that does not exist in the IDAES repository. Custom user models may source from external scripts, import surrogate equations or use first-principles calculations. However, IDAES flowsheets adhere to a standardized modeling hierarchy and simple Pyomo models do not always follow these conventions. Additionally, simple flowsheet submodels often require integration with other IDAES unit models which requires consistency between corresponding port variables, stream properties and physical unit sets, as well as proper usage of `ControlVolume` blocks.\n", + "\n", + "The IDAES `SkeletonUnitModel` allows custom creation of user models blocks that do not require `ControlVolume` blocks, and enabling connection with standard IDAES unit models that do contain `ControlVolume` blocks. To motivate the usefulness and versatility of this tool, we will consider a simple pervaporation unit. The custom model does not require rigorous thermodynamic calculations contained in adjacent unit models, and using a Skeleton model allows definition of only required variables and constraints. The new block does require state variable connections for the inlet and outlet streams. We will demonstrate this scenario below to highlight the usage and benefits of the Skeleton model." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.WATER.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 2. Example - Pervaporation\n", + "\n", + "Pervaporation is a low-energy separation process, and is particularly advantageous over distillation for azeotropic solutions or aqueous mixtures of heavy alcohols. Ethylene glycol is more environmentally friendly than typical chloride- and bromide-based dessicants, and is a common choice for commercial recovery of water from flue gas via liquid spray columns. Due to ethylene glycol's high boiling point, diffusion-based water recovery is economically favorable compared to distillation-based processes. The following example and flux correlation are taken from the literature source below:\n", + "\n", + "Jennifer Runhong Du, Amit Chakma, X. Feng, Dehydration of ethylene glycol by pervaporation using poly(N,N-dimethylaminoethyl methacrylate)/polysulfone composite membranes, Separation and Purification Technology, Volume 64, Issue 1, 2008, Pages 63-70, ISSN 1383-5866, https://doi.org/10.1016/j.seppur.2008.08.004.\n", + "\n", + "The process is adapted from the literature, utilizing an inlet aqueous glycol feed circulated through a feed tank-membrane-feed tank recycle loop while permeate is continuously extracted by the membrane. To demonstrate the usefulness of the Skeleton model, we will model this system as a Mixer and custom Pervaporation unit per the diagram below and define the flux as an empirical custom mass balance term rather than requiring rigorous diffusion calculations. We will also circumvent the need for a vapor phase and VLE calculations by manually calculating the duty to condense and collect permeate vapor, and use correlations for steady-state fluxes to avoid a recycle requiring tear calculations.\n", + "\n", + "![](pervaporation_process.png)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.WATER: Initialization Complete.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2.1 Pyomo and IDAES Imports\n", + "We will begin with relevant imports. We will need basic Pyomo and IDAES components:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.GLYCOL.properties: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pytest\n", + "from pyomo.environ import (\n", + " check_optimal_termination,\n", + " ConcreteModel,\n", + " Constraint,\n", + " Expression,\n", + " Objective,\n", + " maximize,\n", + " Var,\n", + " Set,\n", + " TransformationFactory,\n", + " value,\n", + " exp,\n", + " units as pyunits,\n", + ")\n", + "from pyomo.network import Arc\n", + "from idaes.core import FlowsheetBlock\n", + "from idaes.models.unit_models import Feed, SkeletonUnitModel, Mixer, Product\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.util.initialization import propagate_state\n", + "from idaes.core.solvers import get_solver\n", + "from pyomo.util.check_units import assert_units_consistent\n", + "\n", + "# import thermophysical properties\n", + "import eg_h2o_ideal as thermo_props\n", + "from idaes.models.properties.modular_properties import GenericParameterBlock\n", + "from idaes.core.util.constants import Constants" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.GLYCOL.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2.2 Build Flowsheet\n", + "\n", + "We will build a simple model manually defining state variables relations entering and exiting the pervaporation unit. As shown below, we may define our pre-separation mixer as usual:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.GLYCOL.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# build the flowsheet\n", + "m = ConcreteModel()\n", + "m.fs = FlowsheetBlock(dynamic=False)\n", + "\n", + "m.fs.thermo_params = GenericParameterBlock(**thermo_props.config_dict)\n", + "\n", + "m.fs.WATER = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.GLYCOL = Feed(property_package=m.fs.thermo_params)\n", + "\n", + "m.fs.M101 = Mixer(\n", + " property_package=m.fs.thermo_params, inlet_list=[\"water_feed\", \"glycol_feed\"]\n", + ")\n", + "\n", + "m.fs.RETENTATE = Product(property_package=m.fs.thermo_params)\n", + "m.fs.PERMEATE = Product(property_package=m.fs.thermo_params)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.GLYCOL: Initialization Complete.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2.2 Defining Skeleton Model and Connections\n", + "\n", + "Now that our flowsheet exists, we can manually define variables, units, constraints and ports for our custom pervaporation unit model. By using a Skeleton model, we avoid rigorous mass and energy balances and phase equilibrium which impact model tractability. Instead, we define state variable relations as below - note that we include the fluxes as outlet flow terms. In this model, the variables specify an `FpcTP` system where molar flow of each component, temperature and pressure are selected as state variables:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 11\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 0\n", - "\n", - "Total number of variables............................: 7\n", - " variables with only lower bounds: 0\n", - " variables with lower and upper bounds: 0\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 7\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.13e-16 0.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - "\n", - "Number of Iterations....: 0\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.1275702593849246e-16 1.1275702593849246e-16\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.1275702593849246e-16 1.1275702593849246e-16\n", - "\n", - "\n", - "Number of objective function evaluations = 1\n", - "Number of objective gradient evaluations = 1\n", - "Number of equality constraint evaluations = 1\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 1\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 0\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# define Skeleton model for pervaporation unit\n", + "m.fs.pervap = SkeletonUnitModel(dynamic=False)\n", + "m.fs.pervap.comp_list = Set(initialize=[\"water\", \"ethylene_glycol\"])\n", + "m.fs.pervap.phase_list = Set(initialize=[\"Liq\"])\n", + "\n", + "# input vars for skeleton\n", + "# m.fs.time is a pre-initialized Set belonging to the FlowsheetBlock; for dynamic=False, time=[0]\n", + "m.fs.pervap.flow_in = Var(\n", + " m.fs.time,\n", + " m.fs.pervap.phase_list,\n", + " m.fs.pervap.comp_list,\n", + " initialize=1.0,\n", + " units=pyunits.mol / pyunits.s,\n", + ")\n", + "m.fs.pervap.temperature_in = Var(m.fs.time, initialize=298.15, units=pyunits.K)\n", + "m.fs.pervap.pressure_in = Var(m.fs.time, initialize=101e3, units=pyunits.Pa)\n", + "\n", + "# output vars for skeleton\n", + "m.fs.pervap.perm_flow = Var(\n", + " m.fs.time,\n", + " m.fs.pervap.phase_list,\n", + " m.fs.pervap.comp_list,\n", + " initialize=1.0,\n", + " units=pyunits.mol / pyunits.s,\n", + ")\n", + "m.fs.pervap.ret_flow = Var(\n", + " m.fs.time,\n", + " m.fs.pervap.phase_list,\n", + " m.fs.pervap.comp_list,\n", + " initialize=1.0,\n", + " units=pyunits.mol / pyunits.s,\n", + ")\n", + "m.fs.pervap.temperature_out = Var(m.fs.time, initialize=298.15, units=pyunits.K)\n", + "m.fs.pervap.pressure_out = Var(m.fs.time, initialize=101e3, units=pyunits.Pa)\n", + "m.fs.pervap.vacuum = Var(m.fs.time, initialize=1.3e3, units=pyunits.Pa)\n", + "\n", + "# dictionaries relating state properties to custom variables\n", + "inlet_dict = {\n", + " \"flow_mol_phase_comp\": m.fs.pervap.flow_in,\n", + " \"temperature\": m.fs.pervap.temperature_in,\n", + " \"pressure\": m.fs.pervap.pressure_in,\n", + "}\n", + "retentate_dict = {\n", + " \"flow_mol_phase_comp\": m.fs.pervap.ret_flow,\n", + " \"temperature\": m.fs.pervap.temperature_out,\n", + " \"pressure\": m.fs.pervap.pressure_out,\n", + "}\n", + "permeate_dict = {\n", + " \"flow_mol_phase_comp\": m.fs.pervap.perm_flow,\n", + " \"temperature\": m.fs.pervap.temperature_out,\n", + " \"pressure\": m.fs.pervap.vacuum,\n", + "}\n", + "\n", + "m.fs.pervap.add_ports(name=\"inlet\", member_dict=inlet_dict)\n", + "m.fs.pervap.add_ports(name=\"retentate\", member_dict=retentate_dict)\n", + "m.fs.pervap.add_ports(name=\"permeate\", member_dict=permeate_dict)\n", + "\n", + "# internal vars for skeleton\n", + "energy_activation_dict = {\n", + " (0, \"Liq\", \"water\"): 51e3,\n", + " (0, \"Liq\", \"ethylene_glycol\"): 53e3,\n", + "}\n", + "m.fs.pervap.energy_activation = Var(\n", + " m.fs.time,\n", + " m.fs.pervap.phase_list,\n", + " m.fs.pervap.comp_list,\n", + " initialize=energy_activation_dict,\n", + " units=pyunits.J / pyunits.mol,\n", + ")\n", + "m.fs.pervap.energy_activation.fix()\n", + "\n", + "permeance_dict = {\n", + " (0, \"Liq\", \"water\"): 5611320,\n", + " (0, \"Liq\", \"ethylene_glycol\"): 22358.88,\n", + "} # calculated from literature data\n", + "m.fs.pervap.permeance = Var(\n", + " m.fs.time,\n", + " m.fs.pervap.phase_list,\n", + " m.fs.pervap.comp_list,\n", + " initialize=permeance_dict,\n", + " units=pyunits.mol / pyunits.s / pyunits.m**2,\n", + ")\n", + "m.fs.pervap.permeance.fix()\n", + "\n", + "m.fs.pervap.area = Var(m.fs.time, initialize=6, units=pyunits.m**2)\n", + "m.fs.pervap.area.fix()\n", + "\n", + "latent_heat_dict = {\n", + " (0, \"Liq\", \"water\"): 40.660e3,\n", + " (0, \"Liq\", \"ethylene_glycol\"): 56.9e3,\n", + "}\n", + "m.fs.pervap.latent_heat_of_vaporization = Var(\n", + " m.fs.time,\n", + " m.fs.pervap.phase_list,\n", + " m.fs.pervap.comp_list,\n", + " initialize=latent_heat_dict,\n", + " units=pyunits.J / pyunits.mol,\n", + ")\n", + "m.fs.pervap.latent_heat_of_vaporization.fix()\n", + "m.fs.pervap.heat_duty = Var(\n", + " m.fs.time, initialize=1, units=pyunits.J / pyunits.s\n", + ") # we will calculate this later" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Custom initialization routine complete: Ipopt 3.13.2\\x3a Optimal Solution Found\n", - "2023-11-02 10:27:47 [INFO] idaes.init.fs.PERMEATE.properties: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's define our surrogate equations for flux and permeance, and link them to the port variables. Users can use this structure to write custom relations between inlet and outlet streams; for example, here we define the outlet flow of the pervaporation unit as a sum of the inlet flow and calculated recovery fluxes. By defining model constraints in lieu of rigorous mass balances, we add the flux as a custom mass balance term via an empirical correlation and calculate only the condensation duty rather than implementing full energy balance calculations:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.PERMEATE.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Surrogate and first principles model equations\n", + "\n", + "# flux equation (gas constant is defined as J/mol-K)\n", + "\n", + "\n", + "def rule_permeate_flux(pervap, t, p, i):\n", + " return pervap.permeate.flow_mol_phase_comp[t, p, i] / pervap.area[t] == (\n", + " pervap.permeance[t, p, i]\n", + " * exp(\n", + " -pervap.energy_activation[t, p, i]\n", + " / (Constants.gas_constant * pervap.inlet.temperature[t])\n", + " )\n", + " )\n", + "\n", + "\n", + "m.fs.pervap.eq_permeate_flux = Constraint(\n", + " m.fs.time, m.fs.pervap.phase_list, m.fs.pervap.comp_list, rule=rule_permeate_flux\n", + ")\n", + "\n", + "# permeate condensation equation\n", + "# heat duty based on condensing all of permeate product vapor\n", + "# avoids the need for a Heater or HeatExchanger unit model\n", + "\n", + "\n", + "def rule_duty(pervap, t):\n", + " return pervap.heat_duty[t] == sum(\n", + " pervap.latent_heat_of_vaporization[t, p, i]\n", + " * pervap.permeate.flow_mol_phase_comp[t, p, i]\n", + " for p in pervap.phase_list\n", + " for i in pervap.comp_list\n", + " )\n", + "\n", + "\n", + "m.fs.pervap.eq_duty = Constraint(m.fs.time, rule=rule_duty)\n", + "\n", + "# flow equation adding total recovery as a custom mass balance term\n", + "def rule_retentate_flow(pervap, t, p, i):\n", + " return pervap.retentate.flow_mol_phase_comp[t, p, i] == (\n", + " pervap.inlet.flow_mol_phase_comp[t, p, i]\n", + " - pervap.permeate.flow_mol_phase_comp[t, p, i]\n", + " )\n", + "\n", + "\n", + "m.fs.pervap.eq_retentate_flow = Constraint(\n", + " m.fs.time, m.fs.pervap.phase_list, m.fs.pervap.comp_list, rule=rule_retentate_flow\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.PERMEATE.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, let's define the Arc connecting our two models (IDAES Mixer and custom Pervaporation) and build the flowsheet network:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.PERMEATE: Initialization Complete.\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.s01 = Arc(source=m.fs.WATER.outlet, destination=m.fs.M101.water_feed)\n", + "m.fs.s02 = Arc(source=m.fs.GLYCOL.outlet, destination=m.fs.M101.glycol_feed)\n", + "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.pervap.inlet)\n", + "m.fs.s04 = Arc(source=m.fs.pervap.permeate, destination=m.fs.PERMEATE.inlet)\n", + "m.fs.s05 = Arc(source=m.fs.pervap.retentate, destination=m.fs.RETENTATE.inlet)\n", + "TransformationFactory(\"network.expand_arcs\").apply_to(m)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.RETENTATE.properties: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see how many degrees of freedom the flowsheet has:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.RETENTATE.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(degrees_of_freedom(m))" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.RETENTATE.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2.3 Inlet Specifications\n", + "\n", + "To obtain a square problem with zero degrees of freedom, we specify the inlet water flow, ethylene glycol flow, temperature and pressure for each feed stream, as well as the permeate stream pressure:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:27:47 [INFO] idaes.init.fs.RETENTATE: Initialization Complete.\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.WATER.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(0.34) # mol/s\n", + "m.fs.WATER.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(1e-6) # mol/s\n", + "m.fs.WATER.outlet.temperature.fix(318.15) # K\n", + "m.fs.WATER.outlet.pressure.fix(101.325e3) # Pa\n", + "\n", + "m.fs.GLYCOL.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(1e-6) # mol/s\n", + "m.fs.GLYCOL.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(0.66) # mol/s\n", + "m.fs.GLYCOL.outlet.temperature.fix(318.15) # K\n", + "m.fs.GLYCOL.outlet.pressure.fix(101.325e3) # Pa" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 113\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 74\n", - "\n", - "Total number of variables............................: 47\n", - " variables with only lower bounds: 0\n", - " variables with lower and upper bounds: 33\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 47\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 7.37e+07 0.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 7.03e+05 2.30e+01 -1.0 1.01e+05 - 5.55e-01 9.90e-01h 1\n", - " 2 0.0000000e+00 2.33e+05 1.02e+02 -1.0 1.00e+03 - 7.60e-01 6.58e-01h 1\n", - " 3 0.0000000e+00 6.05e+03 5.91e+01 -1.0 1.56e+03 - 9.90e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 5.50e+01 7.15e+02 -1.0 1.97e+03 - 9.90e-01 1.00e+00h 1\n", - " 5 0.0000000e+00 5.29e-05 2.18e+00 -1.0 1.90e+02 - 1.00e+00 1.00e+00h 1\n", - " 6 0.0000000e+00 4.28e-10 5.17e-03 -3.8 2.59e-01 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 6\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.1421975304747320e-10 4.2843950609494641e-10\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.1421975304747320e-10 4.2843950609494641e-10\n", - "\n", - "\n", - "Number of objective function evaluations = 7\n", - "Number of objective gradient evaluations = 7\n", - "Number of equality constraint evaluations = 7\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 7\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 6\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "# Add this to the imports\n", - "from pyomo.util.calc_var_value import calculate_variable_from_constraint\n", - "\n", - "\n", - "def my_initialize(unit, **kwargs):\n", - " # Callback for user provided initialization sequence\n", - " # Fix the inlet state\n", - " unit.inlet.flow_mol_phase_comp.fix()\n", - " unit.inlet.pressure.fix()\n", - " unit.inlet.temperature.fix()\n", - "\n", - " # Calculate the values of the remaining variables\n", - " for t in m.fs.time:\n", - "\n", - " calculate_variable_from_constraint(\n", - " unit.permeate.flow_mol_phase_comp[t, \"Liq\", \"water\"],\n", - " unit.eq_permeate_flux[t, \"Liq\", \"water\"],\n", - " )\n", - "\n", - " calculate_variable_from_constraint(\n", - " unit.permeate.flow_mol_phase_comp[t, \"Liq\", \"ethylene_glycol\"],\n", - " unit.eq_permeate_flux[t, \"Liq\", \"ethylene_glycol\"],\n", - " )\n", - "\n", - " calculate_variable_from_constraint(unit.heat_duty[t], unit.eq_duty[t])\n", - "\n", - " calculate_variable_from_constraint(\n", - " unit.retentate.flow_mol_phase_comp[t, \"Liq\", \"water\"],\n", - " unit.eq_retentate_flow[t, \"Liq\", \"water\"],\n", - " )\n", - "\n", - " calculate_variable_from_constraint(\n", - " unit.retentate.flow_mol_phase_comp[t, \"Liq\", \"ethylene_glycol\"],\n", - " unit.eq_retentate_flow[t, \"Liq\", \"ethylene_glycol\"],\n", - " )\n", - "\n", - " calculate_variable_from_constraint(\n", - " unit.retentate.temperature[t], unit.temperature_out_calculation[t]\n", - " )\n", - "\n", - " calculate_variable_from_constraint(\n", - " unit.retentate.pressure[t], unit.pressure_out_calculation[t]\n", - " )\n", - "\n", - " assert degrees_of_freedom(unit) == 0\n", - " if degrees_of_freedom(unit) == 0:\n", - " res = solver.solve(unit, tee=True)\n", - " unit.inlet.flow_mol_phase_comp.unfix()\n", - " unit.inlet.temperature.unfix()\n", - " unit.inlet.pressure.unfix()\n", - " print(\"Custom initialization routine complete: \", res.solver.message)\n", - "\n", - "\n", - "solver = get_solver()\n", - "\n", - "m.fs.WATER.initialize()\n", - "propagate_state(m.fs.s01)\n", - "\n", - "m.fs.GLYCOL.initialize()\n", - "propagate_state(m.fs.s02)\n", - "\n", - "m.fs.pervap.config.initializer = my_initialize\n", - "my_initialize(m.fs.pervap)\n", - "propagate_state(m.fs.s03)\n", - "\n", - "m.fs.PERMEATE.initialize()\n", - "propagate_state(m.fs.s04)\n", - "\n", - "m.fs.RETENTATE.initialize()\n", - "\n", - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's check the results:" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Additionally, we need to pass rules defining the temperature and pressure outlets of the pervaporation unit:" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "====================================================================================\n", - "Unit : fs.WATER Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Outlet \n", - " Molar Flowrate ('Liq', 'water') mole / second 0.34000\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 1.0000e-06\n", - " Temperature kelvin 318.15\n", - " Pressure pascal 1.0132e+05\n", - "====================================================================================\n", - "\n", - "====================================================================================\n", - "Unit : fs.GLYCOL Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Outlet \n", - " Molar Flowrate ('Liq', 'water') mole / second 1.0000e-06\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 0.66000\n", - " Temperature kelvin 318.15\n", - " Pressure pascal 1.0132e+05\n", - "====================================================================================\n", - "\n", - "====================================================================================\n", - "Unit : fs.PERMEATE Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet \n", - " Molar Flowrate ('Liq', 'water') mole / second 0.14259\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 0.00026675\n", - " Temperature kelvin 318.15\n", - " Pressure pascal 1300.0\n", - "====================================================================================\n", - "\n", - "====================================================================================\n", - "Unit : fs.RETENTATE Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet \n", - " Molar Flowrate ('Liq', 'water') mole / second 0.19742\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 0.65973\n", - " Temperature kelvin 318.15\n", - " Pressure pascal 1.0132e+05\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "# print results\n", - "\n", - "m.fs.WATER.report()\n", - "m.fs.GLYCOL.report()\n", - "m.fs.PERMEATE.report()\n", - "m.fs.RETENTATE.report()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Add a constraint to calculate the outlet temperature.\n", + "# Here, assume outlet temperature is the same as inlet temperature for illustration\n", + "# in reality, temperature change from latent heat loss through membrane is negligible\n", + "\n", + "\n", + "def rule_temp_out(pervap, t):\n", + " return pervap.inlet.temperature[t] == pervap.retentate.temperature[t]\n", + "\n", + "\n", + "m.fs.pervap.temperature_out_calculation = Constraint(m.fs.time, rule=rule_temp_out)\n", + "\n", + "# Add a constraint to calculate the retentate pressure\n", + "# Here, assume the retentate pressure is the same as the inlet pressure for illustration\n", + "# in reality, pressure change from mass loss through membrane is negligible\n", + "\n", + "\n", + "def rule_pres_out(pervap, t):\n", + " return pervap.inlet.pressure[t] == pervap.retentate.pressure[t]\n", + "\n", + "\n", + "m.fs.pervap.pressure_out_calculation = Constraint(m.fs.time, rule=rule_pres_out)\n", + "\n", + "# fix permeate vacuum pressure\n", + "m.fs.PERMEATE.inlet.pressure.fix(1.3e3)\n", + "\n", + "assert degrees_of_freedom(m) == 0" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Inlet water mole fraction: 0.34000031999936\n", - "Permeate water mole fraction: 0.9981326967912869\n", - "Separation factor: 1037.61881493386\n", - "Condensation duty: 5.81271115195759 kW\n", - "Duty per mole water recovered: 0.011324013423297915 kW-h / mol\n" - ] - } - ], - "source": [ - "# separation factor for results analysis\n", - "m.fs.inlet_water_frac = Expression(\n", - " expr=(\n", - " m.fs.pervap.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - " / sum(\n", - " m.fs.pervap.inlet.flow_mol_phase_comp[0, \"Liq\", i]\n", - " for i in m.fs.pervap.comp_list\n", - " )\n", - " )\n", - ")\n", - "m.fs.permeate_water_frac = Expression(\n", - " expr=(\n", - " m.fs.pervap.permeate.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - " / sum(\n", - " m.fs.pervap.permeate.flow_mol_phase_comp[0, \"Liq\", i]\n", - " for i in m.fs.pervap.comp_list\n", - " )\n", - " )\n", - ")\n", - "m.fs.separation_factor = Expression(\n", - " expr=(m.fs.permeate_water_frac / (1 - m.fs.permeate_water_frac))\n", - " / (m.fs.inlet_water_frac / (1 - m.fs.inlet_water_frac))\n", - ")\n", - "\n", - "print(f\"Inlet water mole fraction: {value(m.fs.inlet_water_frac)}\")\n", - "print(f\"Permeate water mole fraction: {value(m.fs.permeate_water_frac)}\")\n", - "print(f\"Separation factor: {value(m.fs.separation_factor)}\")\n", - "print(f\"Condensation duty: {value(m.fs.pervap.heat_duty[0]/1000)} kW\")\n", - "print(\n", - " f\"Duty per mole water recovered: {value(m.fs.pervap.heat_duty[0]/(1000*m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, 'Liq', 'water']*3600))} kW-h / mol\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "# check results\n", - "assert check_optimal_termination(results)\n", - "assert_units_consistent(m)\n", - "\n", - "assert value(\n", - " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1426, rel=1e-3)\n", - "assert value(\n", - " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.0002667, rel=1e-3)\n", - "assert value(\n", - " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1974, rel=1e-3)\n", - "assert value(\n", - " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.6597, rel=1e-3)\n", - "assert value(m.fs.separation_factor) == pytest.approx(1038, rel=1e-3)\n", - "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5813, rel=1e-3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 3. Optimization\n", - "\n", - "Suppose we wish to characterize the membrane behavior by calculating the maximum inlet water mole fraction allowing a separation factor of at least 100 (typical value for high-efficiency separation processes such as gas separation of CO2/N2). We need to fix total inlet flow to ensure physically-sound solutions. We can quickly modify and resolve the model, and check some key results:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2.4 Custom Initialization\n", + "In addition to allowing custom variable and constraint definitions, the Skeleton model enables implementation of a custom initialization scheme. Complex unit operations may present unique tractability issues, and users have precise control over piecewise unit model solving." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 121\n", - "Number of nonzeros in inequality constraint Jacobian.: 4\n", - "Number of nonzeros in Lagrangian Hessian.............: 88\n", - "\n", - "Total number of variables............................: 49\n", - " variables with only lower bounds: 0\n", - " variables with lower and upper bounds: 35\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 48\n", - "Total number of inequality constraints...............: 1\n", - " inequality constraints with only lower bounds: 1\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 -3.4000032e-01 7.27e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 -3.7368840e-01 7.00e+02 5.28e+01 -1.0 4.41e-02 - 9.90e-01 8.91e-01h 1\n", - " 2 -5.0140067e-01 2.19e+02 1.12e+02 -1.0 2.27e-01 - 9.90e-01 6.56e-01h 1\n", - " 3 -6.1937184e-01 8.96e+00 1.15e+04 -1.0 1.39e-01 - 9.91e-01 9.93e-01h 1\n", - " 4 -6.1863718e-01 1.51e-02 8.34e+01 -1.0 1.28e-03 - 1.00e+00 9.99e-01h 1\n", - " 5 -6.1150206e-01 2.30e-04 7.88e+02 -1.0 8.32e-03 - 1.00e+00 1.00e+00f 1\n", - " 6 -6.1019582e-01 1.79e-06 1.61e+01 -1.0 1.52e-03 - 1.00e+00 1.00e+00h 1\n", - " 7 -7.3642276e-01 1.05e-02 2.29e+04 -2.5 1.47e-01 - 7.70e-01 1.00e+00f 1\n", - " 8 -8.1765712e-01 2.02e-02 9.69e+02 -2.5 1.47e-01 - 1.00e+00 6.43e-01f 1\n", - " 9 -8.3576869e-01 3.85e-03 4.60e+01 -2.5 2.11e-02 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 -8.3954012e-01 1.72e-04 1.05e+00 -2.5 4.40e-03 - 1.00e+00 1.00e+00h 1\n", - " 11 -8.3930724e-01 8.78e-08 2.93e-03 -2.5 2.72e-04 - 1.00e+00 1.00e+00h 1\n", - " 12 -8.4239161e-01 6.01e-05 9.69e+01 -3.8 3.73e-03 - 1.00e+00 9.63e-01f 1\n", - " 13 -8.4225198e-01 7.26e-08 1.37e-02 -3.8 1.63e-04 - 1.00e+00 1.00e+00f 1\n", - " 14 -8.4225232e-01 5.82e-11 3.86e-08 -3.8 3.92e-07 - 1.00e+00 1.00e+00h 1\n", - " 15 -8.4240230e-01 1.48e-07 2.22e-02 -5.7 1.75e-04 - 1.00e+00 1.00e+00f 1\n", - " 16 -8.4240161e-01 5.82e-11 1.66e-08 -5.7 8.12e-07 - 1.00e+00 1.00e+00h 1\n", - " 17 -8.4240336e-01 1.16e-10 3.07e-06 -7.0 2.05e-06 - 1.00e+00 1.00e+00f 1\n", - " 18 -8.4240336e-01 5.82e-11 4.20e-11 -7.0 5.41e-07 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 18\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: -8.4240336290561868e-01 -8.4240336290561868e-01\n", - "Dual infeasibility......: 4.1950443119276315e-11 4.1950443119276315e-11\n", - "Constraint violation....: 1.2786210408238111e-14 5.8207660913467407e-11\n", - "Complementarity.........: 9.0909090909094912e-08 9.0909090909094912e-08\n", - "Overall NLP error.......: 9.0909090909094912e-08 9.0909090909094912e-08\n", - "\n", - "\n", - "Number of objective function evaluations = 19\n", - "Number of objective gradient evaluations = 19\n", - "Number of equality constraint evaluations = 19\n", - "Number of inequality constraint evaluations = 19\n", - "Number of equality constraint Jacobian evaluations = 19\n", - "Number of inequality constraint Jacobian evaluations = 19\n", - "Number of Lagrangian Hessian evaluations = 18\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.003\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "# unfix inlet flows but fix total to prevent divergence during solve\n", - "m.fs.WATER.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].unfix()\n", - "m.fs.GLYCOL.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].unfix()\n", - "m.fs.total_flow = Constraint(\n", - " expr=m.fs.WATER.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - " + m.fs.GLYCOL.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - " == 1 * pyunits.mol / pyunits.s\n", - ")\n", - "\n", - "# set criteria for separation factor\n", - "m.fs.sep_min = Constraint(expr=m.fs.separation_factor >= 100)\n", - "\n", - "# set objective - defaults to minimization\n", - "m.fs.obj = Objective(expr=m.fs.inlet_water_frac, sense=maximize)\n", - "\n", - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Add this to the imports\n", + "from pyomo.util.calc_var_value import calculate_variable_from_constraint\n", + "\n", + "\n", + "def my_initialize(unit, **kwargs):\n", + " # Callback for user provided initialization sequence\n", + " # Fix the inlet state\n", + " unit.inlet.flow_mol_phase_comp.fix()\n", + " unit.inlet.pressure.fix()\n", + " unit.inlet.temperature.fix()\n", + "\n", + " # Calculate the values of the remaining variables\n", + " for t in m.fs.time:\n", + "\n", + " calculate_variable_from_constraint(\n", + " unit.permeate.flow_mol_phase_comp[t, \"Liq\", \"water\"],\n", + " unit.eq_permeate_flux[t, \"Liq\", \"water\"],\n", + " )\n", + "\n", + " calculate_variable_from_constraint(\n", + " unit.permeate.flow_mol_phase_comp[t, \"Liq\", \"ethylene_glycol\"],\n", + " unit.eq_permeate_flux[t, \"Liq\", \"ethylene_glycol\"],\n", + " )\n", + "\n", + " calculate_variable_from_constraint(unit.heat_duty[t], unit.eq_duty[t])\n", + "\n", + " calculate_variable_from_constraint(\n", + " unit.retentate.flow_mol_phase_comp[t, \"Liq\", \"water\"],\n", + " unit.eq_retentate_flow[t, \"Liq\", \"water\"],\n", + " )\n", + "\n", + " calculate_variable_from_constraint(\n", + " unit.retentate.flow_mol_phase_comp[t, \"Liq\", \"ethylene_glycol\"],\n", + " unit.eq_retentate_flow[t, \"Liq\", \"ethylene_glycol\"],\n", + " )\n", + "\n", + " calculate_variable_from_constraint(\n", + " unit.retentate.temperature[t], unit.temperature_out_calculation[t]\n", + " )\n", + "\n", + " calculate_variable_from_constraint(\n", + " unit.retentate.pressure[t], unit.pressure_out_calculation[t]\n", + " )\n", + "\n", + " assert degrees_of_freedom(unit) == 0\n", + " if degrees_of_freedom(unit) == 0:\n", + " res = solver.solve(unit, tee=True)\n", + " unit.inlet.flow_mol_phase_comp.unfix()\n", + " unit.inlet.temperature.unfix()\n", + " unit.inlet.pressure.unfix()\n", + " print(\"Custom initialization routine complete: \", res.solver.message)\n", + "\n", + "\n", + "solver = get_solver()\n", + "\n", + "m.fs.WATER.initialize()\n", + "propagate_state(m.fs.s01)\n", + "\n", + "m.fs.GLYCOL.initialize()\n", + "propagate_state(m.fs.s02)\n", + "\n", + "m.fs.pervap.config.initializer = my_initialize\n", + "my_initialize(m.fs.pervap)\n", + "propagate_state(m.fs.s03)\n", + "\n", + "m.fs.PERMEATE.initialize()\n", + "propagate_state(m.fs.s04)\n", + "\n", + "m.fs.RETENTATE.initialize()\n", + "\n", + "results = solver.solve(m, tee=True)" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "====================================================================================\n", - "Unit : fs.WATER Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Outlet \n", - " Molar Flowrate ('Liq', 'water') mole / second 0.84240\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 1.0000e-06\n", - " Temperature kelvin 318.15\n", - " Pressure pascal 1.0132e+05\n", - "====================================================================================\n", - "\n", - "====================================================================================\n", - "Unit : fs.GLYCOL Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Outlet \n", - " Molar Flowrate ('Liq', 'water') mole / second 1.0000e-06\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 0.15760\n", - " Temperature kelvin 318.15\n", - " Pressure pascal 1.0132e+05\n", - "====================================================================================\n", - "\n", - "====================================================================================\n", - "Unit : fs.PERMEATE Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet \n", - " Molar Flowrate ('Liq', 'water') mole / second 0.14259\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 0.00026675\n", - " Temperature kelvin 318.15\n", - " Pressure pascal 1300.0\n", - "====================================================================================\n", - "\n", - "====================================================================================\n", - "Unit : fs.RETENTATE Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet \n", - " Molar Flowrate ('Liq', 'water') mole / second 0.69982\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 0.15733\n", - " Temperature kelvin 318.15\n", - " Pressure pascal 1.0132e+05\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "# print results\n", - "\n", - "m.fs.WATER.report()\n", - "m.fs.GLYCOL.report()\n", - "m.fs.PERMEATE.report()\n", - "m.fs.RETENTATE.report()" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's check the results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# print results\n", + "\n", + "m.fs.WATER.report()\n", + "m.fs.GLYCOL.report()\n", + "m.fs.PERMEATE.report()\n", + "m.fs.RETENTATE.report()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# separation factor for results analysis\n", + "m.fs.inlet_water_frac = Expression(\n", + " expr=(\n", + " m.fs.pervap.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", + " / sum(\n", + " m.fs.pervap.inlet.flow_mol_phase_comp[0, \"Liq\", i]\n", + " for i in m.fs.pervap.comp_list\n", + " )\n", + " )\n", + ")\n", + "m.fs.permeate_water_frac = Expression(\n", + " expr=(\n", + " m.fs.pervap.permeate.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", + " / sum(\n", + " m.fs.pervap.permeate.flow_mol_phase_comp[0, \"Liq\", i]\n", + " for i in m.fs.pervap.comp_list\n", + " )\n", + " )\n", + ")\n", + "m.fs.separation_factor = Expression(\n", + " expr=(m.fs.permeate_water_frac / (1 - m.fs.permeate_water_frac))\n", + " / (m.fs.inlet_water_frac / (1 - m.fs.inlet_water_frac))\n", + ")\n", + "\n", + "print(f\"Inlet water mole fraction: {value(m.fs.inlet_water_frac)}\")\n", + "print(f\"Permeate water mole fraction: {value(m.fs.permeate_water_frac)}\")\n", + "print(f\"Separation factor: {value(m.fs.separation_factor)}\")\n", + "print(f\"Condensation duty: {value(m.fs.pervap.heat_duty[0]/1000)} kW\")\n", + "print(\n", + " f\"Duty per mole water recovered: {value(m.fs.pervap.heat_duty[0]/(1000*m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, 'Liq', 'water']*3600))} kW-h / mol\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# check results\n", + "assert check_optimal_termination(results)\n", + "assert_units_consistent(m)\n", + "\n", + "assert value(\n", + " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", + ") == pytest.approx(0.14258566, rel=1e-5)\n", + "assert value(\n", + " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", + ") == pytest.approx(0.000266748768, rel=1e-5)\n", + "assert value(\n", + " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", + ") == pytest.approx(0.19741534, rel=1e-5)\n", + "assert value(\n", + " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", + ") == pytest.approx(0.65973425, rel=1e-5)\n", + "assert value(m.fs.separation_factor) == pytest.approx(1037.6188, rel=1e-5)\n", + "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5812.7111, rel=1e-5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 3. Optimization\n", + "\n", + "Suppose we wish to characterize the membrane behavior by calculating the maximum inlet water mole fraction allowing a separation factor of at least 100 (typical value for high-efficiency separation processes such as gas separation of CO2/N2). We need to fix total inlet flow to ensure physically-sound solutions. We can quickly modify and resolve the model, and check some key results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# unfix inlet flows but fix total to prevent divergence during solve\n", + "m.fs.WATER.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].unfix()\n", + "m.fs.GLYCOL.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].unfix()\n", + "m.fs.total_flow = Constraint(\n", + " expr=m.fs.WATER.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", + " + m.fs.GLYCOL.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", + " == 1 * pyunits.mol / pyunits.s\n", + ")\n", + "\n", + "# set criteria for separation factor\n", + "m.fs.sep_min = Constraint(expr=m.fs.separation_factor >= 100)\n", + "\n", + "# set objective - defaults to minimization\n", + "m.fs.obj = Objective(expr=m.fs.inlet_water_frac, sense=maximize)\n", + "\n", + "results = solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# print results\n", + "\n", + "m.fs.WATER.report()\n", + "m.fs.GLYCOL.report()\n", + "m.fs.PERMEATE.report()\n", + "m.fs.RETENTATE.report()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"Inlet water mole fraction: {value(m.fs.inlet_water_frac)}\")\n", + "print(f\"Permeate water mole fraction: {value(m.fs.permeate_water_frac)}\")\n", + "print(f\"Separation factor: {value(m.fs.separation_factor)}\")\n", + "print(f\"Condensation duty: {value(m.fs.pervap.heat_duty[0]/1000)} kW\")\n", + "print(\n", + " f\"Duty per mole water recovered: {value(m.fs.pervap.heat_duty[0]/(1000*m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, 'Liq', 'water']*3600))} kW-h / mol\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# check results\n", + "assert check_optimal_termination(results)\n", + "assert_units_consistent(m)\n", + "\n", + "assert value(\n", + " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", + ") == pytest.approx(0.14258566, rel=1e-5)\n", + "assert value(\n", + " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", + ") == pytest.approx(0.000266748768, rel=1e-5)\n", + "assert value(\n", + " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", + ") == pytest.approx(0.69981938, rel=1e-5)\n", + "assert value(\n", + " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", + ") == pytest.approx(0.15733020, rel=1e-5)\n", + "assert value(m.fs.separation_factor) == pytest.approx(100.000067, rel=1e-5)\n", + "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5812.7111, rel=1e-5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4. Summary\n", + "\n", + "The IDAES Skeleton Unit Model is a powerful tool for implementing relatively simple first-princples, surrogate-based or empirical unit operations. More crucially, users can add their own custom models and integrate them into a larger IDAES flowsheet without adding control volumes or rigorous flow balance and equilibrium calculations when not required. The pervaporation example displays a case where all model equations are empirical correlations or simple manual calculations, with a small number of state variable and port connections, and the Skeleton model avoids complex calculations that impact model tractability. The example also demonstrates adding a custom initialization scheme to handle internally model degrees of freedom, a feature providing greater user control than with most IDAES unit models." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Inlet water mole fraction: 0.8424033629056187\n", - "Permeate water mole fraction: 0.9981326967914326\n", - "Separation factor: 100.00006747653647\n", - "Condensation duty: 5.812711140380676 kW\n", - "Duty per mole water recovered: 0.011324013423295606 kW-h / mol\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Tags", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" } - ], - "source": [ - "print(f\"Inlet water mole fraction: {value(m.fs.inlet_water_frac)}\")\n", - "print(f\"Permeate water mole fraction: {value(m.fs.permeate_water_frac)}\")\n", - "print(f\"Separation factor: {value(m.fs.separation_factor)}\")\n", - "print(f\"Condensation duty: {value(m.fs.pervap.heat_duty[0]/1000)} kW\")\n", - "print(\n", - " f\"Duty per mole water recovered: {value(m.fs.pervap.heat_duty[0]/(1000*m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, 'Liq', 'water']*3600))} kW-h / mol\"\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [], - "source": [ - "# check results\n", - "assert check_optimal_termination(results)\n", - "assert_units_consistent(m)\n", - "\n", - "assert value(\n", - " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1426, rel=1e-3)\n", - "assert value(\n", - " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.0002667, rel=1e-3)\n", - "assert value(\n", - " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.6998, rel=1e-3)\n", - "assert value(\n", - " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.1573, rel=1e-3)\n", - "assert value(m.fs.separation_factor) == pytest.approx(100.0, rel=1e-3)\n", - "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5813, rel=1e-3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 4. Summary\n", - "\n", - "The IDAES Skeleton Unit Model is a powerful tool for implementing relatively simple first-princples, surrogate-based or empirical unit operations. More crucially, users can add their own custom models and integrate them into a larger IDAES flowsheet without adding control volumes or rigorous flow balance and equilibrium calculations when not required. The pervaporation example displays a case where all model equations are empirical correlations or simple manual calculations, with a small number of state variable and port connections, and the Skeleton model avoids complex calculations that impact model tractability. The example also demonstrates adding a custom initialization scheme to handle internally model degrees of freedom, a feature providing greater user control than with most IDAES unit models." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "celltoolbar": "Tags", - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 3 -} + "nbformat": 4, + "nbformat_minor": 3 +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_test.ipynb b/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_test.ipynb index 9d9adfbe..e3cbb4d7 100644 --- a/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_test.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_test.ipynb @@ -583,18 +583,18 @@ "\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1426, rel=1e-3)\n", + ") == pytest.approx(0.14258566, rel=1e-5)\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.0002667, rel=1e-3)\n", + ") == pytest.approx(0.000266748768, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1974, rel=1e-3)\n", + ") == pytest.approx(0.19741534, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.6597, rel=1e-3)\n", - "assert value(m.fs.separation_factor) == pytest.approx(1038, rel=1e-3)\n", - "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5813, rel=1e-3)" + ") == pytest.approx(0.65973425, rel=1e-5)\n", + "assert value(m.fs.separation_factor) == pytest.approx(1037.6188, rel=1e-5)\n", + "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5812.7111, rel=1e-5)" ] }, { @@ -671,18 +671,18 @@ "\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1426, rel=1e-3)\n", + ") == pytest.approx(0.14258566, rel=1e-5)\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.0002667, rel=1e-3)\n", + ") == pytest.approx(0.000266748768, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.6998, rel=1e-3)\n", + ") == pytest.approx(0.69981938, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.1573, rel=1e-3)\n", - "assert value(m.fs.separation_factor) == pytest.approx(100.0, rel=1e-3)\n", - "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5813, rel=1e-3)" + ") == pytest.approx(0.15733020, rel=1e-5)\n", + "assert value(m.fs.separation_factor) == pytest.approx(100.000067, rel=1e-5)\n", + "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5812.7111, rel=1e-5)" ] }, { @@ -719,9 +719,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_usr.ipynb b/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_usr.ipynb index 9d9adfbe..e3cbb4d7 100644 --- a/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_usr.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/operations/skeleton_unit_usr.ipynb @@ -583,18 +583,18 @@ "\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1426, rel=1e-3)\n", + ") == pytest.approx(0.14258566, rel=1e-5)\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.0002667, rel=1e-3)\n", + ") == pytest.approx(0.000266748768, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1974, rel=1e-3)\n", + ") == pytest.approx(0.19741534, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.6597, rel=1e-3)\n", - "assert value(m.fs.separation_factor) == pytest.approx(1038, rel=1e-3)\n", - "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5813, rel=1e-3)" + ") == pytest.approx(0.65973425, rel=1e-5)\n", + "assert value(m.fs.separation_factor) == pytest.approx(1037.6188, rel=1e-5)\n", + "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5812.7111, rel=1e-5)" ] }, { @@ -671,18 +671,18 @@ "\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.1426, rel=1e-3)\n", + ") == pytest.approx(0.14258566, rel=1e-5)\n", "assert value(\n", " m.fs.PERMEATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.0002667, rel=1e-3)\n", + ") == pytest.approx(0.000266748768, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"water\"]\n", - ") == pytest.approx(0.6998, rel=1e-3)\n", + ") == pytest.approx(0.69981938, rel=1e-5)\n", "assert value(\n", " m.fs.RETENTATE.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - ") == pytest.approx(0.1573, rel=1e-3)\n", - "assert value(m.fs.separation_factor) == pytest.approx(100.0, rel=1e-3)\n", - "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5813, rel=1e-3)" + ") == pytest.approx(0.15733020, rel=1e-5)\n", + "assert value(m.fs.separation_factor) == pytest.approx(100.000067, rel=1e-5)\n", + "assert value(m.fs.pervap.heat_duty[0]) == pytest.approx(5812.7111, rel=1e-5)" ] }, { @@ -719,9 +719,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/operations/tests/__init__.py b/idaes_examples/notebooks/docs/unit_models/operations/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/idaes_examples/notebooks/docs/unit_models/operations/tests/test_eg_h2o_ideal.py b/idaes_examples/notebooks/docs/unit_models/operations/tests/test_eg_h2o_ideal.py new file mode 100644 index 00000000..99bbcd25 --- /dev/null +++ b/idaes_examples/notebooks/docs/unit_models/operations/tests/test_eg_h2o_ideal.py @@ -0,0 +1,693 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# +""" +Author: Brandon Paul +""" +import pytest +from pyomo.environ import ( + assert_optimal_termination, + ConcreteModel, + Set, + value, + Var, + units as pyunits, + as_quantity, +) +from pyomo.common.unittest import assertStructuredAlmostEqual + +from idaes.core import Component +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + fixed_variables_set, + activated_constraints_set, +) +from idaes.core.solvers import get_solver + +from idaes.models.properties.modular_properties.base.generic_property import ( + GenericParameterBlock, +) + +from idaes.models.properties.modular_properties.state_definitions import FpcTP + +from idaes_examples.notebooks.docs.unit_models.operations.eg_h2o_ideal import ( + config_dict, +) + +from idaes.models.properties.tests.test_harness import PropertyTestHarness + +from idaes.core.util.model_diagnostics import DiagnosticsToolbox + +from idaes.core import VaporPhase + +from idaes.models.properties.modular_properties.eos.ideal import Ideal + +import copy + + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +class TestEGProdIdeal(PropertyTestHarness): + def configure(self): + self.prop_pack = GenericParameterBlock + self.param_args = config_dict + self.prop_args = {} + self.has_density_terms = True + + +class TestParamBlock(object): + @pytest.mark.unit + def test_build(self): + model = ConcreteModel() + model.params = GenericParameterBlock(**config_dict) + + assert isinstance(model.params.phase_list, Set) + assert len(model.params.phase_list) == 1 + for i in model.params.phase_list: + assert i in [ + "Liq", + ] + assert model.params.Liq.is_liquid_phase() + + assert isinstance(model.params.component_list, Set) + assert len(model.params.component_list) == 2 + for i in model.params.component_list: + assert i in ["water", "ethylene_glycol"] + assert isinstance(model.params.get_component(i), Component) + + assert isinstance(model.params._phase_component_set, Set) + assert len(model.params._phase_component_set) == 2 + for i in model.params._phase_component_set: + assert i in [ + ("Liq", "water"), + ("Liq", "ethylene_glycol"), + ] + + assert model.params.config.state_definition == FpcTP + + assertStructuredAlmostEqual( + model.params.config.state_bounds, + { + "flow_mol_phase_comp": (0, 100, 1000, pyunits.mol / pyunits.s), + "temperature": (273.15, 298.15, 450, pyunits.K), + "pressure": (1e3, 1e5, 1e6, pyunits.Pa), + }, + item_callback=as_quantity, + ) + + assert value(model.params.pressure_ref) == 1e5 + assert value(model.params.temperature_ref) == 298.15 + + assert value(model.params.water.mw) == 18.015e-3 + assert value(model.params.water.pressure_crit) == 221.2e5 + assert value(model.params.water.temperature_crit) == 647.3 + + assert value(model.params.ethylene_glycol.mw) == 62.069e-3 + assert value(model.params.ethylene_glycol.pressure_crit) == 77e5 + assert value(model.params.ethylene_glycol.temperature_crit) == 645 + + dt = DiagnosticsToolbox(model) + dt.assert_no_structural_warnings() + + +class TestStateBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.params = GenericParameterBlock(**config_dict) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Liq", "water"].fix(100) + model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"].fix(100) + model.props[1].temperature.fix(300) + model.props[1].pressure.fix(101325) + + return model + + @pytest.mark.unit + def test_build(self, model): + # Check state variable values and bounds + assert isinstance(model.props[1].flow_mol_phase_comp, Var) + assert value(model.props[1].flow_mol_phase_comp["Liq", "water"]) == 100 + assert ( + value(model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"]) == 100 + ) + assert model.props[1].flow_mol_phase_comp["Liq", "water"].ub == 1000 + assert model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"].ub == 1000 + assert model.props[1].flow_mol_phase_comp["Liq", "water"].lb == 0 + assert model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"].lb == 0 + + assert isinstance(model.props[1].pressure, Var) + assert value(model.props[1].pressure) == 101325 + assert model.props[1].pressure.ub == 1e6 + assert model.props[1].pressure.lb == 1e3 + + assert isinstance(model.props[1].temperature, Var) + assert value(model.props[1].temperature) == 300 + assert model.props[1].temperature.ub == 450 + assert model.props[1].temperature.lb == 273.15 + + @pytest.mark.unit + def test_define_state_vars(self, model): + sv = model.props[1].define_state_vars() + + assert len(sv) == 3 + for i in sv: + assert i in ["flow_mol_phase_comp", "temperature", "pressure"] + + @pytest.mark.unit + def test_define_port_members(self, model): + sv = model.props[1].define_state_vars() + + assert len(sv) == 3 + for i in sv: + assert i in ["flow_mol_phase_comp", "temperature", "pressure"] + + @pytest.mark.unit + def test_define_display_vars(self, model): + sv = model.props[1].define_display_vars() + + assert len(sv) == 3 + for i in sv: + assert i in [ + "Molar Flowrate", + "Temperature", + "Pressure", + ] + + @pytest.mark.unit + def test_structural_diagnostics(self, model): + dt = DiagnosticsToolbox(model) + dt.assert_no_structural_warnings() + + @pytest.mark.unit + def test_basic_scaling(self, model): + assert len(model.props[1].scaling_factor) == 12 + assert model.props[1].scaling_factor[model.props[1].flow_mol] == 1e-2 + assert ( + model.props[1].scaling_factor[model.props[1].flow_mol_comp["water"]] == 1e-2 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].flow_mol_comp["ethylene_glycol"] + ] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[model.props[1].flow_mol_phase["Liq"]] == 1e-2 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].flow_mol_phase_comp["Liq", "water"] + ] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"] + ] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[model.props[1].mole_frac_comp["water"]] + == 1000 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_comp["ethylene_glycol"] + ] + == 1000 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_phase_comp["Liq", "water"] + ] + == 1000 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_phase_comp["Liq", "ethylene_glycol"] + ] + == 1000 + ) + assert model.props[1].scaling_factor[model.props[1].pressure] == 1e-5 + assert model.props[1].scaling_factor[model.props[1].temperature] == 1e-2 + + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_initialize(self, model): + orig_fixed_vars = fixed_variables_set(model) + orig_act_consts = activated_constraints_set(model) + + model.props.initialize(optarg={"tol": 1e-6}) + + assert degrees_of_freedom(model) == 0 + + fin_fixed_vars = fixed_variables_set(model) + fin_act_consts = activated_constraints_set(model) + + assert len(fin_act_consts) == len(orig_act_consts) + assert len(fin_fixed_vars) == len(orig_fixed_vars) + + for c in fin_act_consts: + assert c in orig_act_consts + for v in fin_fixed_vars: + assert v in orig_fixed_vars + + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solve(self, model): + results = solver.solve(model) + + # Check for optimal solution + assert_optimal_termination(results) + + @pytest.mark.unit + def test_numerical_diagnostics(self, model): + dt = DiagnosticsToolbox(model) + dt.assert_no_numerical_warnings() + + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solution(self, model): + # Check results + assert value( + model.props[1].flow_mol_phase_comp["Liq", "water"] + ) == pytest.approx(100, abs=1e-4) + assert value( + model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"] + ) == pytest.approx(100, abs=1e-4) + + assert value(model.props[1].temperature) == pytest.approx(300, abs=1e-4) + assert value(model.props[1].pressure) == pytest.approx(101325, abs=1e-4) + + +class TestPerrysProperties(object): + @pytest.fixture(scope="class") + def density_temperatures(self): + # water, ethylene glycol reference temperatures + # from Perry's Chemical Engineers' Handbook 7th Ed. 2-94 to 2-98 + components = ["water", "ethylene_glycol"] + temperatures = dict(zip(components, [[273.16, 333.15], [260.15, 719.7]])) + + return temperatures + + @pytest.fixture(scope="class") + def densities(self): + # water, ethylene glycol densities from + # Perry's Chemical Engineers' Handbook 7th Ed. 2-94 to 2-98 + components = ["water", "ethylene_glycol"] + densities = dict(zip(components, [[55.583, 54.703], [18.31, 5.234]])) + + return densities + + @pytest.fixture(scope="class") + def heat_capacity_temperatures(self): + # water, ethylene glycol reference temperatures + # from Perry's Chemical Engineers' Handbook 7th Ed. 2-170 to 2-174 + components = ["water", "ethylene_glycol"] + temperatures = dict(zip(components, [[273.16, 533.15], [260.15, 493.15]])) + + return temperatures + + @pytest.fixture(scope="class") + def heat_capacities(self): + # water, ethylene glycol heat capacities from + # Perry's Chemical Engineers' Handbook 7th Ed. 2-170 to 2-174 + components = ["water", "ethylene_glycol"] + heat_capacities = dict( + zip(components, [[0.7615e5, 0.8939e5], [1.36661e5, 2.0598e5]]) + ) + + return heat_capacities + + @pytest.fixture(scope="class") + def heat_capacity_reference(self): + # water, ethylene glycol heat capacities from + # NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["water", "ethylene_glycol"] + heat_capacities = dict(zip(components, [0.7538e5, 0.1498e5])) + + return heat_capacities + + @pytest.fixture(scope="class") + def heat_capacity_reference_temperatures(self): + # water, ethylene glycol reference temperatures + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["water", "ethylene_glycol"] + temperatures = dict(zip(components, [298.0, 298.0])) + + return temperatures + + @pytest.mark.parametrize("component", ["water", "ethylene_glycol"]) + @pytest.mark.parametrize("test_point", [0, 1]) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_liquid_densities( + self, component, test_point, density_temperatures, densities + ): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == component: + pass + else: + config_dict_component_only["components"].pop(key) + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Liq", component].fix(100) + + # change lower bound for testing + model.props[1].temperature.setlb(150) + + model.props[1].temperature.fix(density_temperatures[component][test_point]) + model.props[1].pressure.fix(101325) + + results = solver.solve(model) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + assert value( + pyunits.convert( + model.props[1].dens_mol, to_units=pyunits.kmol / pyunits.m**3 + ) + ) == pytest.approx(densities[component][test_point], rel=1e-4) + + @pytest.mark.parametrize("component", ["water", "ethylene_glycol"]) + @pytest.mark.parametrize("test_point", [0, 1]) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_liquid_heat_capacities_enthalpy( + self, + component, + test_point, + heat_capacity_temperatures, + heat_capacities, + heat_capacity_reference, + heat_capacity_reference_temperatures, + ): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == component: + pass + else: + config_dict_component_only["components"].pop(key) + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Liq", component].fix(100) + + model.props[1].pressure.fix(101325) + + # calculate reference point + + model.props[1].temperature.fix(heat_capacity_reference_temperatures[component]) + + results = solver.solve(model) + + enth_mol_ref = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_ref = heat_capacity_reference_temperatures[component] * pyunits.K + cp_mol_ref = ( + heat_capacity_reference[component] + * 1e-3 + * pyunits.J + / pyunits.mol + / pyunits.K + ) + + # calculate test point + + model.props[1].temperature.fix( + heat_capacity_temperatures[component][test_point] + ) + + results = solver.solve(model) + + enth_mol_test = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_test = heat_capacity_temperatures[component][test_point] * pyunits.K + cp_mol_test = ( + heat_capacities[component][test_point] + * 1e-3 + * pyunits.J + / pyunits.mol + / pyunits.K + ) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + + assert value( + pyunits.convert(enth_mol_test, to_units=pyunits.J / pyunits.mol) + ) == pytest.approx( + value( + pyunits.convert( + 0.5 * (cp_mol_test + cp_mol_ref) * (temp_test - temp_ref) + + enth_mol_ref, + to_units=pyunits.J / pyunits.mol, + ) + ), + rel=1e-1, # using 1e-1 tol to check against trapezoid rule estimation of integral + ) + + +class TestRPP4Properties(object): + @pytest.fixture(scope="class") + def heat_capacity_temperatures(self): + # water, ethylene glycol reference temperatures + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["water", "ethylene_glycol"] + temperatures = dict(zip(components, [[545, 632], [500, 600]])) + + return temperatures + + @pytest.fixture(scope="class") + def heat_capacities(self): + # water, ethylene glycol heat capacities from + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["water", "ethylene_glycol"] + heat_capacities = dict(zip(components, [[35.70, 36.69], [113.64, 125.65]])) + + return heat_capacities + + @pytest.fixture(scope="class") + def heat_capacity_reference(self): + # water, ethylene glycol heat capacities from + # NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["water", "ethylene_glycol"] + heat_capacities = dict(zip(components, [35.22, 97.99])) + + return heat_capacities + + @pytest.fixture(scope="class") + def heat_capacity_reference_temperatures(self): + # water, ethylene glycol reference temperatures + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["water", "ethylene_glycol"] + temperatures = dict(zip(components, [500, 400])) + + return temperatures + + @pytest.fixture(scope="class") + def saturation_pressure_temperatures(self): + # water, ethylene glycol reference temperatures + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["water", "ethylene_glycol"] + temperatures = dict(zip(components, [[300.25, 350.16], [387, 473]])) + + return temperatures + + @pytest.fixture(scope="class") + def saturation_pressures(self): + # ethylene glycol saturation pressures from + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["water", "ethylene_glycol"] + pressures = dict( + zip(components, [[0.03591e5, 0.4194e5], [0.04257e5, 1.0934e5]]) + ) + + return pressures + + @pytest.mark.parametrize("component", ["water", "ethylene_glycol"]) + @pytest.mark.parametrize("test_point", [0, 1]) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_vapor_heat_capacities_enthalpy( + self, + component, + test_point, + heat_capacity_temperatures, + heat_capacities, + heat_capacity_reference, + heat_capacity_reference_temperatures, + ): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == component: + pass + else: + config_dict_component_only["components"].pop(key) + + config_dict_component_only["phases"] = { + "Vap": {"type": VaporPhase, "equation_of_state": Ideal} + } + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Vap", component].fix(100) + + model.props[1].pressure.fix(101325) + + # calculate reference point + + model.props[1].temperature.fix(heat_capacity_reference_temperatures[component]) + + results = solver.solve(model) + + enth_mol_ref = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_ref = heat_capacity_reference_temperatures[component] * pyunits.K + cp_mol_ref = ( + heat_capacity_reference[component] + * 1e-3 + * pyunits.J + / pyunits.mol + / pyunits.K + ) + + # calculate test point + + model.props[1].temperature.fix( + heat_capacity_temperatures[component][test_point] + ) + + results = solver.solve(model) + + enth_mol_test = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_test = heat_capacity_temperatures[component][test_point] * pyunits.K + cp_mol_test = ( + heat_capacities[component][test_point] + * 1e-3 + * pyunits.J + / pyunits.mol + / pyunits.K + ) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + + assert value( + pyunits.convert(enth_mol_test, to_units=pyunits.J / pyunits.mol) + ) == pytest.approx( + value( + pyunits.convert( + 0.5 * (cp_mol_test + cp_mol_ref) * (temp_test - temp_ref) + + enth_mol_ref, + to_units=pyunits.J / pyunits.mol, + ) + ), + rel=1.15e-1, # using 1.15e-1 tol to check against trapezoid rule estimation of integral + # all values match within 1e-1, except ethylene glycol test point 0 + ) + + @pytest.mark.parametrize("component", ["water", "ethylene_glycol"]) + @pytest.mark.parametrize("test_point", [0, 1]) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_saturation_pressures( + self, + component, + test_point, + saturation_pressure_temperatures, + saturation_pressures, + ): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == component: + pass + else: + config_dict_component_only["components"].pop(key) + + config_dict_component_only["phases"] = { + "Vap": {"type": VaporPhase, "equation_of_state": Ideal} + } + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Vap", component].fix(100) + + model.props[1].temperature.fix( + saturation_pressure_temperatures[component][test_point] + ) + model.props[1].pressure.fix(101325) + + results = solver.solve(model) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + print(value(model.props[1].pressure_sat_comp[component])) + assert value(model.props[1].pressure_sat_comp[component]) == pytest.approx( + saturation_pressures[component][test_point], rel=1.5e-2 + ) # match within 1.5% diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/cstr.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/cstr.ipynb index 95e99c96..0d0fc742 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/cstr.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/cstr.ipynb @@ -237,9 +237,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = CSTR(\n", @@ -584,7 +582,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" ] }, { @@ -599,7 +597,7 @@ "source": [ "import pytest\n", "\n", - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(3.458138, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(8.004012, rel=1e-5)" ] }, { @@ -622,8 +620,8 @@ "print()\n", "print(\n", " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.3f} gal\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.6f} gal\"\n", ")" ] }, @@ -637,10 +635,10 @@ }, "outputs": [], "source": [ - "assert value(m.fs.R101.conversion) == pytest.approx(0.8000, abs=1e-3)\n", - "assert value(m.fs.R101.volume[0]) == pytest.approx(5.5380, abs=1e-3)\n", - "assert value(m.fs.R101.heat_duty[0]) / 1e6 == pytest.approx(-5.6566, abs=1e-3)\n", - "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2827, abs=1e-3)" + "assert value(m.fs.R101.conversion) == pytest.approx(0.8000, rel=1e-5)\n", + "assert value(m.fs.R101.volume[0]) == pytest.approx(5.5380, rel=1e-5)\n", + "assert value(m.fs.R101.heat_duty[0]) / 1e6 == pytest.approx(-5.8675, rel=1e-5)\n", + "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.29261, rel=1e-5)" ] }, { @@ -751,7 +749,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", "\n", "print()\n", "print(\"Heater results\")\n", @@ -774,8 +772,8 @@ }, "outputs": [], "source": [ - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(3.889709, abs=1e-3)\n", - "assert value(m.fs.R101.volume[0]) == pytest.approx(18.927, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(8.318177, rel=1e-5)\n", + "assert value(m.fs.R101.volume[0]) == pytest.approx(18.927, rel=1e-5)" ] }, { @@ -794,17 +792,17 @@ "print(\"Optimal Values\")\n", "print()\n", "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", "print(\n", " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.3f} gal\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.6f} gal\"\n", ")\n", "\n", "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", "\n", "print()\n", "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" @@ -820,10 +818,10 @@ }, "outputs": [], "source": [ - "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, abs=1e-3)\n", - "assert value(m.fs.R101.volume[0] * 1.2) == pytest.approx(22.712, abs=1e-3)\n", - "assert value(m.fs.eg_prod) == pytest.approx(225.415, abs=1e-3)\n", - "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.0, abs=1e-3)" + "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, rel=1e-5)\n", + "assert value(m.fs.R101.volume[0] * 1.2) == pytest.approx(22.712471, rel=1e-5)\n", + "assert value(m.fs.eg_prod) == pytest.approx(225.415, rel=1e-5)\n", + "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.0, rel=1e-5)" ] }, { @@ -851,7 +849,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/cstr_doc.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/cstr_doc.ipynb index 0186ee75..891c3495 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/cstr_doc.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/cstr_doc.ipynb @@ -1,1254 +1,723 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "tags": [ - "header", - "hide-cell" - ] - }, - "outputs": [], - "source": [ - "###############################################################################\n", - "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", - "# Framework (IDAES IP) was produced under the DOE Institute for the\n", - "# Design of Advanced Energy Systems (IDAES).\n", - "#\n", - "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", - "# University of California, through Lawrence Berkeley National Laboratory,\n", - "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", - "# University, West Virginia University Research Corporation, et al.\n", - "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", - "# for full copyright and license information.\n", - "###############################################################################" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Flowsheet Continuous Stirred Tank Reactor (CSTR) Simulation and Optimization of Ethylene Glycol Production\n", - "Author: Brandon Paul \n", - "Maintainer: Brandon Paul \n", - "Updated: 2023-06-01 \n", - "\n", - "\n", - "## Learning Outcomes\n", - "\n", - "\n", - "- Call and implement the IDAES CSTR unit model\n", - "- Construct a steady-state flowsheet using the IDAES unit model library\n", - "- Connecting unit models in a flowsheet using Arcs\n", - "- Fomulate and solve an optimization problem\n", - " - Defining an objective function\n", - " - Setting variable bounds\n", - " - Adding additional constraints \n", - "\n", - "\n", - "## Problem Statement\n", - "\n", - "This example is adapted from Fogler, H.S., Elements of Chemical Reaction Engineering 5th ed., 2016, Prentice Hall, p. 157-160.\n", - "\n", - "Ethylene glycol (EG) is a high-demand chemical, with billions of pounds produced every year for applications such as vehicle anti-freeze. EG may be readily obtained from the hydrolysis of ethylene oxide in the presence of a catalytic intermediate. In this example, an aqueous solution of ethylene oxide hydrolizes after mixing with an aqueous solution of sulfuric acid catalyst:\n", - "\n", - "**C2H4O + H2O + H2SO4 → C2H6O2 + H2SO4**\n", - "\n", - "This reaction often occurs by two mechanisms, as the catalyst may bind to either reactant before the final hydrolysis step; we will simplify the reaction to a single step for this example.\n", - "\n", - "The flowsheet that we will be using for this module is shown below with the stream conditions. We will be processing ethylene oxide and catalyst solutions of fixed concentrations to produce 200 MM lb/year of EG. As shown in the flowsheet, the process consists of a mixer M101 for the two inlet streams, a heater H101 to preheat the feed to the reaction temperature, and a CSTR unit R101 with an external cooling system to remove heat generated by the exothermic reaction. We will assume ideal solutions and thermodynamics for this flowsheet, as well as well-mixed liquid behavior (no vapor phase) in the reactor. The properties required for this module are available in the same directory:\n", - "\n", - "- egprod_ideal.py\n", - "- egprod_reaction.py\n", - "\n", - "The state variables chosen for the property package are **molar flows of each component by phase in each stream, temperature of each stream and pressure of each stream**. The components considered are: **ethylene oxide, water, sulfuric acid and ethylene glycol** and the process occurs in liquid phase only. Therefore, every stream has 4 flow variables, 1 temperature and 1 pressure variable. \n", - "\n", - "![](egprod_flowsheet.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Pyomo and IDAES components\n", - "\n", - "\n", - "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages. Let us first import the following components from Pyomo:\n", - "- Constraint (to write constraints)\n", - "- Var (to declare variables)\n", - "- ConcreteModel (to create the concrete model object)\n", - "- Expression (to evaluate values as a function of variables defined in the model)\n", - "- Objective (to define an objective function for optimization)\n", - "- TransformationFactory (to apply certain transformations)\n", - "- Arc (to connect two unit models)\n", - "\n", - "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\n", - "\n", - "From idaes, we will be needing the `FlowsheetBlock` and the following unit models:\n", - "- Mixer\n", - "- Heater\n", - "- CSTR\n", - "\n", - "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom, tools for model expressions and calling variable values, and built-in functions to define property packages, add unit containers to objects and define our initialization scheme.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from pyomo.environ import (\n", - " Constraint,\n", - " Var,\n", - " ConcreteModel,\n", - " Expression,\n", - " Objective,\n", - " TransformationFactory,\n", - " value,\n", - " units as pyunits,\n", - ")\n", - "from pyomo.network import Arc\n", - "\n", - "from idaes.core import FlowsheetBlock\n", - "from idaes.models.properties.modular_properties import (\n", - " GenericParameterBlock,\n", - " GenericReactionParameterBlock,\n", - ")\n", - "from idaes.models.unit_models import Feed, Mixer, Heater, CSTR, Product\n", - "\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.util.initialization import propagate_state" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Thermophysical and Reaction Packages\n", - "\n", - "The final step is to import the thermophysical and reaction packages. We have created a custom thermophysical package that support ideal vapor and liquid behavior for this system, and in this case we will restrict it to ideal liquid behavior only.\n", - "\n", - "The reaction package here assumes Arrhenius kinetic behavior for the CSTR, for which $k_0$ and $E_a$ are known *a priori* (if unknown, they may be obtained using one of the parameter estimation tools within IDAES).\n", - "\n", - "$ r = -kVC_{EO} $, $ k = k_0 e^{(-E_a/RT)}$, with the variables as follows:\n", - "\n", - "$r$ - reaction rate extent in moles of ethylene oxide consumed per second; note that the traditional reaction rate would be given by $rate = r/V$ in moles per $m^3$ per second \n", - "$k$ - reaction rate constant per second \n", - "$V$ - volume of CSTR in $m^3$, note that this is *liquid volume* and not the *total volume* of the reactor itself \n", - "$C_{EO}$ - bulk concentration of ethylene oxide in moles per $m^3$ (the limiting reagent, since we assume excess catalyst and water) \n", - "$k_0$ - pre-exponential Arrhenius factor per second \n", - "$E_a$ - reaction activation energy in kJ per mole of ethylene oxide consumed \n", - "$R$ - gas constant in J/mol-K \n", - "$T$ - reactor temperature in K\n", - "\n", - "These calculations are contained within the property, reaction and unit model packages, and do not need to be entered into the flowsheet. More information on property estimation may be found in the IDAES documentation on [Parameter Estimation](https://idaes-pse.readthedocs.io/en/stable/how_to_guides/workflow/data_rec_parmest.html).\n", - "\n", - "ParamEst parameter estimation: \n", - "\n", - "Let us import the following modules from the same directory as this Jupyter notebook:\n", - "- egprod_ideal as thermo_props\n", - "- egprod_reaction as reaction_props" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import egprod_ideal as thermo_props\n", - "import egprod_reaction as reaction_props" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Constructing the Flowsheet\n", - "\n", - "We have now imported all the components, unit models, and property modules we need to construct a flowsheet. Let us create a `ConcreteModel` and add the flowsheet block. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "m = ConcreteModel()\n", - "m.fs = FlowsheetBlock(dynamic=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now need to add the property packages to the flowsheet. Unlike the basic [Flash unit model example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Tutorials/Basics/flash_unit_solution_testing_doc.md), where we only had a thermophysical property package, for this flowsheet we will also need to add a reaction property package. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) and [Modular Reaction Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-reaction-package-framework). The get_prop method for the natural gas property module automatically returns the correct dictionary using a component list argument. The GenericParameterBlock and GenericReactionParameterBlock methods build states blocks from passed parameter data; the reaction block unpacks using **reaction_props.config_dict to allow for optional or empty keyword arguments:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.fs.thermo_params = GenericParameterBlock(**thermo_props.config_dict)\n", - "m.fs.reaction_params = GenericReactionParameterBlock(\n", - " property_package=m.fs.thermo_params, **reaction_props.config_dict\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Unit Models\n", - "\n", - "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding a `Mixer`, a `Heater` and a `CSTR`. Note that all unit models need to be given a property package argument. In addition to that, there are several arguments depending on the unit model, please refer to the documentation for more details on [IDAES Unit Models](https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/index.html). For example, the `Mixer` is given a `list` consisting of names to the two inlets." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.fs.OXIDE = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.ACID = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", - "m.fs.M101 = Mixer(\n", - " property_package=m.fs.thermo_params, inlet_list=[\"reagent_feed\", \"catalyst_feed\"]\n", - ")\n", - "m.fs.H101 = Heater(\n", - " property_package=m.fs.thermo_params,\n", - " has_pressure_change=False,\n", - " has_phase_equilibrium=False,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "m.fs.R101 = CSTR(\n", - " property_package=m.fs.thermo_params,\n", - " reaction_package=m.fs.reaction_params,\n", - " has_heat_of_reaction=True,\n", - " has_heat_transfer=True,\n", - " has_pressure_change=False,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Connecting Unit Models Using Arcs\n", - "\n", - "We have now added all the unit models we need to the flowsheet. However, we have not yet specified how the units are to be connected. To do this, we will be using the `Arc` which is a pyomo component that takes in two arguments: `source` and `destination`. Let us connect the outlet of the `Mixer` to the inlet of the `Heater`, and the outlet of the `Heater` to the inlet of the `CSTR`. Additionally, we will connect the `Feed` and `Product` blocks to the flowsheet:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.s01 = Arc(source=m.fs.OXIDE.outlet, destination=m.fs.M101.reagent_feed)\n", - "m.fs.s02 = Arc(source=m.fs.ACID.outlet, destination=m.fs.M101.catalyst_feed)\n", - "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)\n", - "m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", - "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We have now connected the unit model block using the arcs. However, we also need to link the state variables on connected ports. Pyomo provides a convenient method `TransformationFactory` to write these equality constraints for us between two ports:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "TransformationFactory(\"network.expand_arcs\").apply_to(m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Expressions to Compute Operating Costs\n", - "\n", - "In this section, we will add a few Expressions that allows us to evaluate the performance. `Expressions` provide a convenient way of calculating certain values that are a function of the variables defined in the model. For more details on `Expressions`, please refer to the [Pyomo Expression documentation]( https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html).\n", - "\n", - "For this flowsheet, we are interested in computing ethylene glycol production in millions of pounds per year, as well as the total costs due to cooling and heating utilities." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us first add an `Expression` to convert the product flow from mol/s to MM lb/year of ethylene glycol. We see that the molecular weight exists in the thermophysical property package, so we may use that value for our calculations." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.eg_prod = Expression(\n", - " expr=pyunits.convert(\n", - " m.fs.PROD.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - " * m.fs.thermo_params.ethylene_glycol.mw, # MW defined in properties as kg/mol\n", - " to_units=pyunits.Mlb / pyunits.yr,\n", - " )\n", - ") # converting kg/s to MM lb/year" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, let us add expressions to compute the reactor cooling cost (\\\\$/s) assuming a cost of 2.12E-5 \\\\$/kW, and the heating utility cost (\\\\$/s) assuming 2.2E-4 \\\\$/kW. Note that the heat duty is in units of watt (J/s). The total operating cost will be the sum of the two, expressed in \\\\$/year assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.cooling_cost = Expression(\n", - " expr=2.12e-8 * (-m.fs.R101.heat_duty[0])\n", - ") # the reaction is exothermic, so R101 duty is negative\n", - "m.fs.heating_cost = Expression(\n", - " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", - ") # the stream must be heated to T_rxn, so H101 duty is positive\n", - "m.fs.operating_cost = Expression(\n", - " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost))\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Feed Conditions\n", - "\n", - "Let us first check how many degrees of freedom exist for this flowsheet using the `degrees_of_freedom` tool we imported earlier. We expect each stream to have 6 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 1 (duty or conversion, since the inlet is also the outlet of H101). In this case, the reactor has an extra degree of freedom (reactor conversion or reactor volume) since we have not yet defined the CSTR performance equation. Therefore, we have 15 degrees of freedom to specify: temperature, pressure and flow of all four components on both streams; outlet heater temperature; reactor conversion and volume." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "15\n" - ] + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "header", + "hide-cell" + ] + }, + "outputs": [], + "source": [ + "###############################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "###############################################################################" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "# Flowsheet Continuous Stirred Tank Reactor (CSTR) Simulation and Optimization of Ethylene Glycol Production\n", + "Author: Brandon Paul \n", + "Maintainer: Brandon Paul \n", + "Updated: 2023-06-01 \n", + "\n", + "\n", + "## Learning Outcomes\n", + "\n", + "\n", + "- Call and implement the IDAES CSTR unit model\n", + "- Construct a steady-state flowsheet using the IDAES unit model library\n", + "- Connecting unit models in a flowsheet using Arcs\n", + "- Fomulate and solve an optimization problem\n", + " - Defining an objective function\n", + " - Setting variable bounds\n", + " - Adding additional constraints \n", + "\n", + "\n", + "## Problem Statement\n", + "\n", + "This example is adapted from Fogler, H.S., Elements of Chemical Reaction Engineering 5th ed., 2016, Prentice Hall, p. 157-160.\n", + "\n", + "Ethylene glycol (EG) is a high-demand chemical, with billions of pounds produced every year for applications such as vehicle anti-freeze. EG may be readily obtained from the hydrolysis of ethylene oxide in the presence of a catalytic intermediate. In this example, an aqueous solution of ethylene oxide hydrolizes after mixing with an aqueous solution of sulfuric acid catalyst:\n", + "\n", + "**C2H4O + H2O + H2SO4 \u2192 C2H6O2 + H2SO4**\n", + "\n", + "This reaction often occurs by two mechanisms, as the catalyst may bind to either reactant before the final hydrolysis step; we will simplify the reaction to a single step for this example.\n", + "\n", + "The flowsheet that we will be using for this module is shown below with the stream conditions. We will be processing ethylene oxide and catalyst solutions of fixed concentrations to produce 200 MM lb/year of EG. As shown in the flowsheet, the process consists of a mixer M101 for the two inlet streams, a heater H101 to preheat the feed to the reaction temperature, and a CSTR unit R101 with an external cooling system to remove heat generated by the exothermic reaction. We will assume ideal solutions and thermodynamics for this flowsheet, as well as well-mixed liquid behavior (no vapor phase) in the reactor. The properties required for this module are available in the same directory:\n", + "\n", + "- egprod_ideal.py\n", + "- egprod_reaction.py\n", + "\n", + "The state variables chosen for the property package are **molar flows of each component by phase in each stream, temperature of each stream and pressure of each stream**. The components considered are: **ethylene oxide, water, sulfuric acid and ethylene glycol** and the process occurs in liquid phase only. Therefore, every stream has 4 flow variables, 1 temperature and 1 pressure variable. \n", + "\n", + "![](egprod_flowsheet.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Pyomo and IDAES components\n", + "\n", + "\n", + "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages. Let us first import the following components from Pyomo:\n", + "- Constraint (to write constraints)\n", + "- Var (to declare variables)\n", + "- ConcreteModel (to create the concrete model object)\n", + "- Expression (to evaluate values as a function of variables defined in the model)\n", + "- Objective (to define an objective function for optimization)\n", + "- TransformationFactory (to apply certain transformations)\n", + "- Arc (to connect two unit models)\n", + "\n", + "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\n", + "\n", + "From idaes, we will be needing the `FlowsheetBlock` and the following unit models:\n", + "- Mixer\n", + "- Heater\n", + "- CSTR\n", + "\n", + "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom, tools for model expressions and calling variable values, and built-in functions to define property packages, add unit containers to objects and define our initialization scheme.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pyomo.environ import (\n", + " Constraint,\n", + " Var,\n", + " ConcreteModel,\n", + " Expression,\n", + " Objective,\n", + " TransformationFactory,\n", + " value,\n", + " units as pyunits,\n", + ")\n", + "from pyomo.network import Arc\n", + "\n", + "from idaes.core import FlowsheetBlock\n", + "from idaes.models.properties.modular_properties import (\n", + " GenericParameterBlock,\n", + " GenericReactionParameterBlock,\n", + ")\n", + "from idaes.models.unit_models import Feed, Mixer, Heater, CSTR, Product\n", + "\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.util.initialization import propagate_state" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Thermophysical and Reaction Packages\n", + "\n", + "The final step is to import the thermophysical and reaction packages. We have created a custom thermophysical package that support ideal vapor and liquid behavior for this system, and in this case we will restrict it to ideal liquid behavior only.\n", + "\n", + "The reaction package here assumes Arrhenius kinetic behavior for the CSTR, for which $k_0$ and $E_a$ are known *a priori* (if unknown, they may be obtained using one of the parameter estimation tools within IDAES).\n", + "\n", + "$ r = -kVC_{EO} $, $ k = k_0 e^{(-E_a/RT)}$, with the variables as follows:\n", + "\n", + "$r$ - reaction rate extent in moles of ethylene oxide consumed per second; note that the traditional reaction rate would be given by $rate = r/V$ in moles per $m^3$ per second \n", + "$k$ - reaction rate constant per second \n", + "$V$ - volume of CSTR in $m^3$, note that this is *liquid volume* and not the *total volume* of the reactor itself \n", + "$C_{EO}$ - bulk concentration of ethylene oxide in moles per $m^3$ (the limiting reagent, since we assume excess catalyst and water) \n", + "$k_0$ - pre-exponential Arrhenius factor per second \n", + "$E_a$ - reaction activation energy in kJ per mole of ethylene oxide consumed \n", + "$R$ - gas constant in J/mol-K \n", + "$T$ - reactor temperature in K\n", + "\n", + "These calculations are contained within the property, reaction and unit model packages, and do not need to be entered into the flowsheet. More information on property estimation may be found in the IDAES documentation on [Parameter Estimation](https://idaes-pse.readthedocs.io/en/stable/how_to_guides/workflow/data_rec_parmest.html).\n", + "\n", + "ParamEst parameter estimation: \n", + "\n", + "Let us import the following modules from the same directory as this Jupyter notebook:\n", + "- egprod_ideal as thermo_props\n", + "- egprod_reaction as reaction_props" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import egprod_ideal as thermo_props\n", + "import egprod_reaction as reaction_props" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constructing the Flowsheet\n", + "\n", + "We have now imported all the components, unit models, and property modules we need to construct a flowsheet. Let us create a `ConcreteModel` and add the flowsheet block. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = ConcreteModel()\n", + "m.fs = FlowsheetBlock(dynamic=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to add the property packages to the flowsheet. Unlike the basic [Flash unit model example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Tutorials/Basics/flash_unit_solution_testing_doc.md), where we only had a thermophysical property package, for this flowsheet we will also need to add a reaction property package. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) and [Modular Reaction Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-reaction-package-framework). The get_prop method for the natural gas property module automatically returns the correct dictionary using a component list argument. The GenericParameterBlock and GenericReactionParameterBlock methods build states blocks from passed parameter data; the reaction block unpacks using **reaction_props.config_dict to allow for optional or empty keyword arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "m.fs.thermo_params = GenericParameterBlock(**thermo_props.config_dict)\n", + "m.fs.reaction_params = GenericReactionParameterBlock(\n", + " property_package=m.fs.thermo_params, **reaction_props.config_dict\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Unit Models\n", + "\n", + "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding a `Mixer`, a `Heater` and a `CSTR`. Note that all unit models need to be given a property package argument. In addition to that, there are several arguments depending on the unit model, please refer to the documentation for more details on [IDAES Unit Models](https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/index.html). For example, the `Mixer` is given a `list` consisting of names to the two inlets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "m.fs.OXIDE = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.ACID = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", + "m.fs.M101 = Mixer(\n", + " property_package=m.fs.thermo_params, inlet_list=[\"reagent_feed\", \"catalyst_feed\"]\n", + ")\n", + "m.fs.H101 = Heater(\n", + " property_package=m.fs.thermo_params,\n", + " has_pressure_change=False,\n", + " has_phase_equilibrium=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101 = CSTR(\n", + " property_package=m.fs.thermo_params,\n", + " reaction_package=m.fs.reaction_params,\n", + " has_heat_of_reaction=True,\n", + " has_heat_transfer=True,\n", + " has_pressure_change=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Connecting Unit Models Using Arcs\n", + "\n", + "We have now added all the unit models we need to the flowsheet. However, we have not yet specified how the units are to be connected. To do this, we will be using the `Arc` which is a pyomo component that takes in two arguments: `source` and `destination`. Let us connect the outlet of the `Mixer` to the inlet of the `Heater`, and the outlet of the `Heater` to the inlet of the `CSTR`. Additionally, we will connect the `Feed` and `Product` blocks to the flowsheet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.s01 = Arc(source=m.fs.OXIDE.outlet, destination=m.fs.M101.reagent_feed)\n", + "m.fs.s02 = Arc(source=m.fs.ACID.outlet, destination=m.fs.M101.catalyst_feed)\n", + "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)\n", + "m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", + "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have now connected the unit model block using the arcs. However, we also need to link the state variables on connected ports. Pyomo provides a convenient method `TransformationFactory` to write these equality constraints for us between two ports:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "TransformationFactory(\"network.expand_arcs\").apply_to(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Expressions to Compute Operating Costs\n", + "\n", + "In this section, we will add a few Expressions that allows us to evaluate the performance. `Expressions` provide a convenient way of calculating certain values that are a function of the variables defined in the model. For more details on `Expressions`, please refer to the [Pyomo Expression documentation]( https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html).\n", + "\n", + "For this flowsheet, we are interested in computing ethylene glycol production in millions of pounds per year, as well as the total costs due to cooling and heating utilities." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us first add an `Expression` to convert the product flow from mol/s to MM lb/year of ethylene glycol. We see that the molecular weight exists in the thermophysical property package, so we may use that value for our calculations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.eg_prod = Expression(\n", + " expr=pyunits.convert(\n", + " m.fs.PROD.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", + " * m.fs.thermo_params.ethylene_glycol.mw, # MW defined in properties as kg/mol\n", + " to_units=pyunits.Mlb / pyunits.yr,\n", + " )\n", + ") # converting kg/s to MM lb/year" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let us add expressions to compute the reactor cooling cost (\\\\$/s) assuming a cost of 2.12E-5 \\\\$/kW, and the heating utility cost (\\\\$/s) assuming 2.2E-4 \\\\$/kW. Note that the heat duty is in units of watt (J/s). The total operating cost will be the sum of the two, expressed in \\\\$/year assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.cooling_cost = Expression(\n", + " expr=2.12e-8 * (-m.fs.R101.heat_duty[0])\n", + ") # the reaction is exothermic, so R101 duty is negative\n", + "m.fs.heating_cost = Expression(\n", + " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", + ") # the stream must be heated to T_rxn, so H101 duty is positive\n", + "m.fs.operating_cost = Expression(\n", + " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost))\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Feed Conditions\n", + "\n", + "Let us first check how many degrees of freedom exist for this flowsheet using the `degrees_of_freedom` tool we imported earlier. We expect each stream to have 6 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 1 (duty or conversion, since the inlet is also the outlet of H101). In this case, the reactor has an extra degree of freedom (reactor conversion or reactor volume) since we have not yet defined the CSTR performance equation. Therefore, we have 15 degrees of freedom to specify: temperature, pressure and flow of all four components on both streams; outlet heater temperature; reactor conversion and volume." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "print(degrees_of_freedom(m))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now be fixing the feed stream to the conditions shown in the flowsheet above. As mentioned in other tutorials, the IDAES framework expects a time index value for every referenced internal stream or unit variable, even in steady-state systems with a single time point $ t = 0 $ (`t = [0]` is the default when creating a `FlowsheetBlock` without passing a `time_set` argument). The non-present components in each stream are assigned a very small non-zero value to help with convergence and initializing. Based on stoichiometric ratios for the reaction, 80% conversion and 200 MM lb/year (46.4 mol/s) of ethylene glycol, we will initialize our simulation with the following calculated values:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", + " 58.0 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", + " 39.6 * pyunits.mol / pyunits.s\n", + ") # calculated from 16.1 mol EO / cudm in stream\n", + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.OXIDE.outlet.temperature.fix(298.15 * pyunits.K)\n", + "m.fs.OXIDE.outlet.pressure.fix(1e5 * pyunits.Pa)\n", + "\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", + " 200 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", + " 0.334 * pyunits.mol / pyunits.s\n", + ") # calculated from 0.9 wt% SA in stream\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.ACID.outlet.temperature.fix(298.15 * pyunits.K)\n", + "m.fs.ACID.outlet.pressure.fix(1e5 * pyunits.Pa)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Unit Model Specifications\n", + "\n", + "Now that we have fixed our inlet feed conditions, we will now be fixing the operating conditions for the unit models in the flowsheet. Let us fix the outlet temperature of H101 to 328.15 K. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.H101.outlet.temperature.fix(328.15 * pyunits.K)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We'll add constraints defining the reactor volume and conversion in relation to the stream properties. Particularly, we want to use our CSTR performance relation: \n", + "\n", + "$V = \\frac{v_0 X} {k(1-X)}$, where the `CSTR` reaction volume $V$ will be specified, the inlet volumetric flow $v_0$ is determined by stream properties, $k$ is calculated by the reaction package, and $X$ will be calculated. Reactor volume is commonly selected as a specification in simulation problems, and choosing conversion is often to perform reactor design.\n", + "\n", + "For the `CSTR`, we have to define the conversion in terms of ethylene oxide as well as the `CSTR` reaction volume. This requires us to create new variables and constraints relating reactor properties to stream properties. Note that the `CSTR` reaction volume variable (m.fs.R101.volume) does not need to be defined here since it is internally defined by the `CSTR` model. Additionally, the heat duty is not fixed, since the heat of reaction depends on the reactor conversion (through the extent of reaction and heat of reaction). We'll estimate 80% conversion for our initial flowsheet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.conversion = Var(\n", + " initialize=0.80, bounds=(0, 1), units=pyunits.dimensionless\n", + ") # fraction\n", + "\n", + "m.fs.R101.conv_constraint = Constraint(\n", + " expr=m.fs.R101.conversion\n", + " * m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", + " == (\n", + " m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", + " - m.fs.R101.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", + " )\n", + ")\n", + "\n", + "m.fs.R101.conversion.fix(0.80)\n", + "\n", + "m.fs.R101.volume.fix(5.538 * pyunits.m**3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(degrees_of_freedom(m))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we need to initialize the each unit operation in sequence to solve the flowsheet. As in best practice, unit operations are initialized or solved, and outlet properties are propagated to connected inlet streams via arc definitions as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize and solve each unit operation\n", + "m.fs.OXIDE.initialize()\n", + "propagate_state(arc=m.fs.s01)\n", + "\n", + "m.fs.ACID.initialize()\n", + "propagate_state(arc=m.fs.s01)\n", + "\n", + "m.fs.M101.initialize()\n", + "propagate_state(arc=m.fs.s03)\n", + "\n", + "m.fs.H101.initialize()\n", + "propagate_state(arc=m.fs.s04)\n", + "\n", + "m.fs.R101.initialize()\n", + "propagate_state(arc=m.fs.s05)\n", + "\n", + "m.fs.PROD.initialize()\n", + "\n", + "# set solver\n", + "solver = get_solver()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Solve the model\n", + "results = solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyze the Results of the Square Problem\n", + "\n", + "\n", + "What is the total operating cost? " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this operating cost, what conversion did we achieve of ethylene oxide to ethylene glycol? " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.report()\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")\n", + "print()\n", + "print(\n", + " f\"Assuming a 20% design factor for reactor volume,\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.6f} gal\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimizing Ethylene Glycol Production\n", + "\n", + "Now that the flowsheet has been squared and solved, we can run a small optimization problem to minimize our production costs. Suppose we require at least 200 million pounds/year of ethylene glycol produced and 90% conversion of ethylene oxide, allowing for variable reactor volume (considering operating/non-capital costs only) and reactor temperature (heater outlet)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us declare our objective function for this problem. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.objective = Objective(expr=m.fs.operating_cost)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now, as well as set bounds for the design variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.eg_prod_con = Constraint(\n", + " expr=m.fs.eg_prod >= 200 * pyunits.Mlb / pyunits.yr\n", + ") # MM lb/year\n", + "m.fs.R101.conversion.fix(0.90)\n", + "\n", + "m.fs.R101.volume.unfix()\n", + "m.fs.R101.volume.setlb(0 * pyunits.m**3)\n", + "m.fs.R101.volume.setub(pyunits.convert(5000 * pyunits.gal, to_units=pyunits.m**3))\n", + "\n", + "m.fs.H101.outlet.temperature.unfix()\n", + "m.fs.H101.outlet.temperature[0].setlb(328.15 * pyunits.K)\n", + "m.fs.H101.outlet.temperature[0].setub(\n", + " 470.45 * pyunits.K\n", + ") # highest component boiling point (ethylene glycol)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "We have now defined the optimization problem and we are now ready to solve this problem. \n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "results = solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", + "\n", + "print()\n", + "print(\"Heater results\")\n", + "\n", + "m.fs.H101.report()\n", + "\n", + "print()\n", + "print(\"CSTR reactor results\")\n", + "\n", + "m.fs.R101.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display optimal values for the decision variables and design variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Optimal Values\")\n", + "print()\n", + "\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", + "\n", + "print()\n", + "print(\n", + " f\"Assuming a 20% design factor for reactor volume,\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.6f} gal\"\n", + ")\n", + "\n", + "print()\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will now be fixing the feed stream to the conditions shown in the flowsheet above. As mentioned in other tutorials, the IDAES framework expects a time index value for every referenced internal stream or unit variable, even in steady-state systems with a single time point $ t = 0 $ (`t = [0]` is the default when creating a `FlowsheetBlock` without passing a `time_set` argument). The non-present components in each stream are assigned a very small non-zero value to help with convergence and initializing. Based on stoichiometric ratios for the reaction, 80% conversion and 200 MM lb/year (46.4 mol/s) of ethylene glycol, we will initialize our simulation with the following calculated values:" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", - " 58.0 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", - " 39.6 * pyunits.mol / pyunits.s\n", - ") # calculated from 16.1 mol EO / cudm in stream\n", - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.OXIDE.outlet.temperature.fix(298.15 * pyunits.K)\n", - "m.fs.OXIDE.outlet.pressure.fix(1e5 * pyunits.Pa)\n", - "\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", - " 200 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", - " 0.334 * pyunits.mol / pyunits.s\n", - ") # calculated from 0.9 wt% SA in stream\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.ACID.outlet.temperature.fix(298.15 * pyunits.K)\n", - "m.fs.ACID.outlet.pressure.fix(1e5 * pyunits.Pa)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Unit Model Specifications\n", - "\n", - "Now that we have fixed our inlet feed conditions, we will now be fixing the operating conditions for the unit models in the flowsheet. Let us fix the outlet temperature of H101 to 328.15 K. " - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.H101.outlet.temperature.fix(328.15 * pyunits.K)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We'll add constraints defining the reactor volume and conversion in relation to the stream properties. Particularly, we want to use our CSTR performance relation: \n", - "\n", - "$V = \\frac{v_0 X} {k(1-X)}$, where the `CSTR` reaction volume $V$ will be specified, the inlet volumetric flow $v_0$ is determined by stream properties, $k$ is calculated by the reaction package, and $X$ will be calculated. Reactor volume is commonly selected as a specification in simulation problems, and choosing conversion is often to perform reactor design.\n", - "\n", - "For the `CSTR`, we have to define the conversion in terms of ethylene oxide as well as the `CSTR` reaction volume. This requires us to create new variables and constraints relating reactor properties to stream properties. Note that the `CSTR` reaction volume variable (m.fs.R101.volume) does not need to be defined here since it is internally defined by the `CSTR` model. Additionally, the heat duty is not fixed, since the heat of reaction depends on the reactor conversion (through the extent of reaction and heat of reaction). We'll estimate 80% conversion for our initial flowsheet:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.R101.conversion = Var(\n", - " initialize=0.80, bounds=(0, 1), units=pyunits.dimensionless\n", - ") # fraction\n", - "\n", - "m.fs.R101.conv_constraint = Constraint(\n", - " expr=m.fs.R101.conversion\n", - " * m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", - " == (\n", - " m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", - " - m.fs.R101.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", - " )\n", - ")\n", - "\n", - "m.fs.R101.conversion.fix(0.80)\n", - "\n", - "m.fs.R101.volume.fix(5.538 * pyunits.m**3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0\n" - ] + ], + "metadata": { + "celltoolbar": "Tags", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we need to initialize the each unit operation in sequence to solve the flowsheet. As in best practice, unit operations are initialized or solved, and outlet properties are propagated to connected inlet streams via arc definitions as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.OXIDE.properties: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.OXIDE.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.OXIDE.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.OXIDE: Initialization Complete.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.ACID.properties: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.ACID.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.ACID.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.ACID: Initialization Complete.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.M101.reagent_feed_state: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.M101.reagent_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.M101.catalyst_feed_state: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.M101.catalyst_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.R101.control_volume.reactions: Initialization Complete.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:22:02 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" - ] - } - ], - "source": [ - "# Initialize and solve each unit operation\n", - "m.fs.OXIDE.initialize()\n", - "propagate_state(arc=m.fs.s01)\n", - "\n", - "m.fs.ACID.initialize()\n", - "propagate_state(arc=m.fs.s01)\n", - "\n", - "m.fs.M101.initialize()\n", - "propagate_state(arc=m.fs.s03)\n", - "\n", - "m.fs.H101.initialize()\n", - "propagate_state(arc=m.fs.s04)\n", - "\n", - "m.fs.R101.initialize()\n", - "propagate_state(arc=m.fs.s05)\n", - "\n", - "m.fs.PROD.initialize()\n", - "\n", - "# set solver\n", - "solver = get_solver()" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 345\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 393\n", - "\n", - "Total number of variables............................: 96\n", - " variables with only lower bounds: 0\n", - " variables with lower and upper bounds: 87\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 96\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.30e+06 0.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 2.66e+06 1.65e+01 -1.0 9.75e+06 - 6.77e-02 9.90e-01h 1\n", - " 2 0.0000000e+00 2.36e+04 2.90e+02 -1.0 9.74e+04 - 7.00e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 2.43e+02 1.43e+03 -1.0 9.75e+02 - 9.75e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 1.85e+00 3.22e+03 -1.0 1.07e+01 - 9.90e-01 9.92e-01h 1\n", - " 5 0.0000000e+00 7.45e-08 4.66e+03 -1.0 8.41e-02 - 9.92e-01 1.00e+00h 1\n", - "Cannot recompute multipliers for feasibility problem. Error in eq_mult_calculator\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 1.6686895357338362e+06 1.6686895357338362e+06\n", - "Constraint violation....: 1.5633344889834636e-09 7.4505805969238281e-08\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.5633344889834636e-09 1.6686895357338362e+06\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.003\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "# Solve the model\n", - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Analyze the Results of the Square Problem\n", - "\n", - "\n", - "What is the total operating cost? " - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $3.458 million per year\n" - ] - } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For this operating cost, what conversion did we achieve of ethylene oxide to ethylene glycol? " - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "====================================================================================\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : -5.6566e+06 : watt : False : (None, None)\n", - " Volume : 5.5380 : meter ** 3 : True : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Molar Flowrate ('Liq', 'ethylene_oxide') mole / second 58.000 11.600\n", - " Molar Flowrate ('Liq', 'water') mole / second 239.60 193.20\n", - " Molar Flowrate ('Liq', 'sulfuric_acid') mole / second 0.33401 0.33401\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 2.0000e-05 46.400\n", - " Temperature kelvin 328.15 328.27\n", - " Pressure pascal 1.0000e+05 1.0000e+05\n", - "====================================================================================\n", - "\n", - "Conversion achieved = 80.0%\n", - "\n", - "Assuming a 20% design factor for reactor volume,total CSTR volume required = 6.646 m^3 = 1755.582 gal\n" - ] - } - ], - "source": [ - "m.fs.R101.report()\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")\n", - "print()\n", - "print(\n", - " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.3f} gal\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Optimizing Ethylene Glycol Production\n", - "\n", - "Now that the flowsheet has been squared and solved, we can run a small optimization problem to minimize our production costs. Suppose we require at least 200 million pounds/year of ethylene glycol produced and 90% conversion of ethylene oxide, allowing for variable reactor volume (considering operating/non-capital costs only) and reactor temperature (heater outlet)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us declare our objective function for this problem. " - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.objective = Objective(expr=m.fs.operating_cost)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now, as well as set bounds for the design variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.eg_prod_con = Constraint(\n", - " expr=m.fs.eg_prod >= 200 * pyunits.Mlb / pyunits.yr\n", - ") # MM lb/year\n", - "m.fs.R101.conversion.fix(0.90)\n", - "\n", - "m.fs.R101.volume.unfix()\n", - "m.fs.R101.volume.setlb(0 * pyunits.m**3)\n", - "m.fs.R101.volume.setub(pyunits.convert(5000 * pyunits.gal, to_units=pyunits.m**3))\n", - "\n", - "m.fs.H101.outlet.temperature.unfix()\n", - "m.fs.H101.outlet.temperature[0].setlb(328.15 * pyunits.K)\n", - "m.fs.H101.outlet.temperature[0].setub(\n", - " 470.45 * pyunits.K\n", - ") # highest component boiling point (ethylene glycol)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "We have now defined the optimization problem and we are now ready to solve this problem. \n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 348\n", - "Number of nonzeros in inequality constraint Jacobian.: 1\n", - "Number of nonzeros in Lagrangian Hessian.............: 408\n", - "\n", - "Total number of variables............................: 98\n", - " variables with only lower bounds: 0\n", - " variables with lower and upper bounds: 89\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 96\n", - "Total number of inequality constraints...............: 1\n", - " inequality constraints with only lower bounds: 1\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 3.4581382e+06 1.76e+06 6.34e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 3.4605407e+06 1.75e+06 1.17e+01 -1.0 6.97e+05 - 7.82e-02 6.15e-03h 1\n", - " 2 3.4957712e+06 1.61e+06 5.18e+01 -1.0 6.96e+05 - 6.78e-02 8.29e-02h 1\n", - " 3 3.5296145e+06 1.47e+06 7.10e+01 -1.0 6.42e+05 - 3.96e-01 8.63e-02h 1\n", - " 4 3.6874124e+06 8.26e+05 2.09e+03 -1.0 5.90e+05 - 7.61e-01 4.38e-01h 1\n", - " 5 3.8876849e+06 1.02e+04 2.43e+03 -1.0 3.31e+05 - 9.39e-01 9.90e-01h 1\n", - " 6 3.8896921e+06 8.92e+01 2.75e+00 -1.0 3.31e+03 - 9.90e-01 9.91e-01h 1\n", - " 7 3.8897098e+06 3.14e-05 1.82e+03 -1.0 2.89e+01 - 9.91e-01 1.00e+00h 1\n", - " 8 3.8897096e+06 1.38e-06 3.37e-04 -1.7 1.42e-01 - 1.00e+00 1.00e+00f 1\n", - " 9 3.8897096e+06 8.68e-08 2.12e-05 -5.7 3.57e-02 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 3.8897096e+06 2.79e-09 4.13e-07 -7.0 6.61e-06 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 10\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 3.8897095750395539e+06 3.8897095750395539e+06\n", - "Dual infeasibility......: 4.1300208155718159e-07 4.1300208155718159e-07\n", - "Constraint violation....: 8.9819498380703823e-15 2.7939677238464360e-09\n", - "Complementarity.........: 9.0909160154398524e-08 9.0909160154398524e-08\n", - "Overall NLP error.......: 9.0909160154398524e-08 4.1300208155718159e-07\n", - "\n", - "\n", - "Number of objective function evaluations = 11\n", - "Number of objective gradient evaluations = 11\n", - "Number of equality constraint evaluations = 11\n", - "Number of inequality constraint evaluations = 11\n", - "Number of equality constraint Jacobian evaluations = 11\n", - "Number of inequality constraint Jacobian evaluations = 11\n", - "Number of Lagrangian Hessian evaluations = 10\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.003\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $3.890 million per year\n", - "\n", - "Heater results\n", - "\n", - "====================================================================================\n", - "Unit : fs.H101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : 699.26 : watt : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Molar Flowrate ('Liq', 'ethylene_oxide') mole / second 58.000 58.000\n", - " Molar Flowrate ('Liq', 'water') mole / second 239.60 239.60\n", - " Molar Flowrate ('Liq', 'sulfuric_acid') mole / second 0.33401 0.33401\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 2.0000e-05 2.0000e-05\n", - " Temperature kelvin 298.15 328.15\n", - " Pressure pascal 1.0000e+05 1.0000e+05\n", - "====================================================================================\n", - "\n", - "CSTR reactor results\n", - "\n", - "====================================================================================\n", - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : -6.3635e+06 : watt : False : (None, None)\n", - " Volume : 18.927 : meter ** 3 : False : (0.0, 18.927058919999997)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Molar Flowrate ('Liq', 'ethylene_oxide') mole / second 58.000 5.8000\n", - " Molar Flowrate ('Liq', 'water') mole / second 239.60 187.40\n", - " Molar Flowrate ('Liq', 'sulfuric_acid') mole / second 0.33401 0.33401\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 2.0000e-05 52.200\n", - " Temperature kelvin 328.15 338.37\n", - " Pressure pascal 1.0000e+05 1.0000e+05\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", - "\n", - "print()\n", - "print(\"Heater results\")\n", - "\n", - "m.fs.H101.report()\n", - "\n", - "print()\n", - "print(\"CSTR reactor results\")\n", - "\n", - "m.fs.R101.report()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Display optimal values for the decision variables and design variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimal Values\n", - "\n", - "H101 outlet temperature = 328.150 K\n", - "\n", - "Assuming a 20% design factor for reactor volume,total CSTR volume required = 22.712 m^3 = 6000.000 gal\n", - "\n", - "Ethylene glycol produced = 225.415 MM lb/year\n", - "\n", - "Conversion achieved = 90.0%\n" - ] - } - ], - "source": [ - "print(\"Optimal Values\")\n", - "print()\n", - "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", - "\n", - "print()\n", - "print(\n", - " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.3f} gal\"\n", - ")\n", - "\n", - "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "celltoolbar": "Tags", - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 3 -} + "nbformat": 4, + "nbformat_minor": 3 +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/cstr_test.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/cstr_test.ipynb index 517309b5..a77d3809 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/cstr_test.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/cstr_test.ipynb @@ -54,7 +54,7 @@ "\n", "Ethylene glycol (EG) is a high-demand chemical, with billions of pounds produced every year for applications such as vehicle anti-freeze. EG may be readily obtained from the hydrolysis of ethylene oxide in the presence of a catalytic intermediate. In this example, an aqueous solution of ethylene oxide hydrolizes after mixing with an aqueous solution of sulfuric acid catalyst:\n", "\n", - "**C2H4O + H2O + H2SO4 → C2H6O2 + H2SO4**\n", + "**C2H4O + H2O + H2SO4 \u2192 C2H6O2 + H2SO4**\n", "\n", "This reaction often occurs by two mechanisms, as the catalyst may bind to either reactant before the final hydrolysis step; we will simplify the reaction to a single step for this example.\n", "\n", @@ -237,9 +237,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = CSTR(\n", @@ -584,7 +582,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" ] }, { @@ -599,7 +597,7 @@ "source": [ "import pytest\n", "\n", - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(3.458138, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(8.004012, rel=1e-5)" ] }, { @@ -622,8 +620,8 @@ "print()\n", "print(\n", " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.3f} gal\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.6f} gal\"\n", ")" ] }, @@ -637,10 +635,10 @@ }, "outputs": [], "source": [ - "assert value(m.fs.R101.conversion) == pytest.approx(0.8000, abs=1e-3)\n", - "assert value(m.fs.R101.volume[0]) == pytest.approx(5.5380, abs=1e-3)\n", - "assert value(m.fs.R101.heat_duty[0]) / 1e6 == pytest.approx(-5.6566, abs=1e-3)\n", - "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2827, abs=1e-3)" + "assert value(m.fs.R101.conversion) == pytest.approx(0.8000, rel=1e-5)\n", + "assert value(m.fs.R101.volume[0]) == pytest.approx(5.5380, rel=1e-5)\n", + "assert value(m.fs.R101.heat_duty[0]) / 1e6 == pytest.approx(-5.8675, rel=1e-5)\n", + "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.29261, rel=1e-5)" ] }, { @@ -751,7 +749,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", "\n", "print()\n", "print(\"Heater results\")\n", @@ -774,8 +772,8 @@ }, "outputs": [], "source": [ - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(3.889709, abs=1e-3)\n", - "assert value(m.fs.R101.volume[0]) == pytest.approx(18.927, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(8.318177, rel=1e-5)\n", + "assert value(m.fs.R101.volume[0]) == pytest.approx(18.927, rel=1e-5)" ] }, { @@ -794,17 +792,17 @@ "print(\"Optimal Values\")\n", "print()\n", "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", "print(\n", " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.3f} gal\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.6f} gal\"\n", ")\n", "\n", "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", "\n", "print()\n", "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" @@ -820,10 +818,10 @@ }, "outputs": [], "source": [ - "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, abs=1e-3)\n", - "assert value(m.fs.R101.volume[0] * 1.2) == pytest.approx(22.712, abs=1e-3)\n", - "assert value(m.fs.eg_prod) == pytest.approx(225.415, abs=1e-3)\n", - "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.0, abs=1e-3)" + "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, rel=1e-5)\n", + "assert value(m.fs.R101.volume[0] * 1.2) == pytest.approx(22.712471, rel=1e-5)\n", + "assert value(m.fs.eg_prod) == pytest.approx(225.415, rel=1e-5)\n", + "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.0, rel=1e-5)" ] }, { @@ -851,9 +849,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/cstr_usr.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/cstr_usr.ipynb index 40b3f0a5..946169b9 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/cstr_usr.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/cstr_usr.ipynb @@ -54,7 +54,7 @@ "\n", "Ethylene glycol (EG) is a high-demand chemical, with billions of pounds produced every year for applications such as vehicle anti-freeze. EG may be readily obtained from the hydrolysis of ethylene oxide in the presence of a catalytic intermediate. In this example, an aqueous solution of ethylene oxide hydrolizes after mixing with an aqueous solution of sulfuric acid catalyst:\n", "\n", - "**C2H4O + H2O + H2SO4 → C2H6O2 + H2SO4**\n", + "**C2H4O + H2O + H2SO4 \u2192 C2H6O2 + H2SO4**\n", "\n", "This reaction often occurs by two mechanisms, as the catalyst may bind to either reactant before the final hydrolysis step; we will simplify the reaction to a single step for this example.\n", "\n", @@ -237,9 +237,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = CSTR(\n", @@ -540,7 +538,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" ] }, { @@ -563,8 +561,8 @@ "print()\n", "print(\n", " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.3f} gal\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.6f} gal\"\n", ")" ] }, @@ -647,7 +645,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", "\n", "print()\n", "print(\"Heater results\")\n", @@ -676,17 +674,17 @@ "print(\"Optimal Values\")\n", "print()\n", "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", "print(\n", " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.3f} gal\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume[0]):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume[0], to_units=pyunits.gal)):0.6f} gal\"\n", ")\n", "\n", "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", "\n", "print()\n", "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" @@ -717,9 +715,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/egprod_ideal.py b/idaes_examples/notebooks/docs/unit_models/reactors/egprod_ideal.py index d9aab2f4..1c18b1fc 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/egprod_ideal.py +++ b/idaes_examples/notebooks/docs/unit_models/reactors/egprod_ideal.py @@ -45,10 +45,9 @@ # 4th edition, Chemical Engineering Series - Robert C. Reid # [2] Perry's Chemical Engineers' Handbook 7th Ed. # [3] NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ -# Retrieved 23rd September, 2021 -# [4] B. Ruscic and D. H. Bross, Active Thermochemical Tables (ATcT) -# values based on ver. 1.122r of the Thermochemical Network (2021); -# available at ATcT.anl.gov +# Retrieved 18th March, 2024 +# [4] Chemeo - Chemical properties of Sulfuric Acid, +# https://www.chemeo.com/cid/24-837-6/Sulfuric-Acid # [5] CRC Handbook of Chemistry and Physics, 97th Ed., W.M. Haynes # [6] Journal of Physical and Chemical Reference Data 20, 1157 # (1991); https:// doi.org/10.1063/1.555899 @@ -56,175 +55,233 @@ config_dict = { # Specifying components "components": { - 'ethylene_oxide': - {"type": Component, - "elemental_composition": {"C": 2, "H": 4, "O": 1}, - "dens_mol_liq_comp": Perrys, - "enth_mol_liq_comp": Perrys, - "enth_mol_ig_comp": RPP4, - "pressure_sat_comp": RPP4, - "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, - "parameter_data": { - "mw": (44.054E-3, pyunits.kg/pyunits.mol), # [1] - "pressure_crit": (71.9e5, pyunits.Pa), # [1] - "temperature_crit": (469, pyunits.K), # [1] - "dens_mol_liq_comp_coeff": { - 'eqn_type': 1, - '1': (1.1836, pyunits.kmol*pyunits.m**-3), # [2] pg. 2-98 - '2': (0.26024, None), - '3': (469.15, pyunits.K), - '4': (0.2696, None)}, - "cp_mol_ig_comp_coeff": { - 'A': (-7.519E0, pyunits.J/pyunits.mol/pyunits.K), # [1] - 'B': (2.222E-1, pyunits.J/pyunits.mol/pyunits.K**2), - 'C': (-1.256E-4, pyunits.J/pyunits.mol/pyunits.K**3), - 'D': (2.592E-8, pyunits.J/pyunits.mol/pyunits.K**4)}, - "cp_mol_liq_comp_coeff": { - '1': (1.4471E2, pyunits.J/pyunits.kmol/pyunits.K), # [2] - '2': (-7.5887E-1, pyunits.J/pyunits.kmol/pyunits.K**2), - '3': (2.8261E-3, pyunits.J/pyunits.kmol/pyunits.K**3), - '4': (-3.064E-6, pyunits.J/pyunits.kmol/pyunits.K**4), - '5': (0, pyunits.J/pyunits.kmol/pyunits.K**5)}, - "enth_mol_form_liq_comp_ref": ( - -95.7e3, pyunits.J/pyunits.mol), # [4] - "enth_mol_form_vap_comp_ref": ( - -52.61e3, pyunits.J/pyunits.mol), # [3] - "pressure_sat_comp_coeff": {'A': (-6.56234, None), # [1] - 'B': (0.42696, None), - 'C': (-1.25638, None), - 'D': (-3.18133, None)}}}, - 'water': - {"type": Component, - "elemental_composition": {"H": 2, "O": 1}, - "dens_mol_liq_comp": Perrys, - "enth_mol_liq_comp": Perrys, - "enth_mol_ig_comp": RPP4, - "pressure_sat_comp": RPP4, - "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, - "parameter_data": { - "mw": (18.015E-3, pyunits.kg/pyunits.mol), # [1] - "pressure_crit": (221.2e5, pyunits.Pa), # [1] - "temperature_crit": (647.3, pyunits.K), # [1] - "dens_mol_liq_comp_coeff": { - 'eqn_type': 2, - '1': (-13.851, pyunits.kmol/pyunits.m**3), # [2]pg. 2-98 - '2': (0.64038, pyunits.kmol/pyunits.m**3/pyunits.K), - '3': (-0.00191, pyunits.kmol/pyunits.m**3/pyunits.K**2), - '4': (1.8211E-6, pyunits.kmol/pyunits.m**3/pyunits.K**3)}, - "cp_mol_ig_comp_coeff": { - 'A': (3.194E1, pyunits.J/pyunits.mol/pyunits.K), # [1] - 'B': (1.436E-3, pyunits.J/pyunits.mol/pyunits.K**2), - 'C': (2.432E-5, pyunits.J/pyunits.mol/pyunits.K**3), - 'D': (-1.176E-8, pyunits.J/pyunits.mol/pyunits.K**4)}, - "cp_mol_liq_comp_coeff": { - '1': (2.7637E2, pyunits.J/pyunits.kmol/pyunits.K), # [2] - '2': (-2.0901, pyunits.J/pyunits.kmol/pyunits.K**2), - '3': (8.125E-3, pyunits.J/pyunits.kmol/pyunits.K**3), - '4': (-1.4116E-5, pyunits.J/pyunits.kmol/pyunits.K**4), - '5': (9.3701E-9, pyunits.J/pyunits.kmol/pyunits.K**5)}, - "enth_mol_form_liq_comp_ref": ( - -285.83e3, pyunits.J/pyunits.mol), # [3] - "enth_mol_form_vap_comp_ref": ( - -241.836e3, pyunits.J/pyunits.mol), # [3] - "pressure_sat_comp_coeff": {'A': (-7.76451, None), # [1] - 'B': (1.45838, None), - 'C': (-2.77580, None), - 'D': (-1.23303, None)}}}, - 'sulfuric_acid': - {"type": Component, - "elemental_composition": {"H": 2, "S": 1, "O": 4}, - "dens_mol_liq_comp": Perrys, # fitted to this equation form - "enth_mol_liq_comp": Perrys, # fitted to this equation form - "enth_mol_ig_comp": NIST, - "pressure_sat_comp": RPP4, # fitted to this equation form - "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, - "parameter_data": { - "mw": (98.078E-3, pyunits.kg/pyunits.mol), # [4] - "pressure_crit": (129.4262e5, pyunits.Pa), # [4] - "temperature_crit": (590.76, pyunits.K), # [4] - "dens_mol_liq_comp_coeff": { - 'eqn_type': 2, - '1': (23.669, pyunits.kmol/pyunits.m**3), # [5] - '2': (-2.5307E-2, pyunits.kmol/pyunits.m**3/pyunits.K), - '3': (3.3523E-4, pyunits.kmol/pyunits.m**3/pyunits.K**2), - '4': (-1.8538E-7, pyunits.kmol/pyunits.m**3/pyunits.K**3)}, - "cp_mol_ig_comp_coeff": { - 'A': (47.28924, pyunits.J/pyunits.mol/pyunits.K), # [3] - 'B': (190.3314, pyunits.J/pyunits.mol/pyunits.K/pyunits.kK), - 'C': (-148.1299, pyunits.J/pyunits.mol/pyunits.K/pyunits.kK**2), - 'D': (43.86631, pyunits.J/pyunits.mol/pyunits.K/pyunits.kK**3), - 'E': (-0.740016, pyunits.J/pyunits.mol/pyunits.K/pyunits.kK**-2), - 'F': (-758.9525, pyunits.kJ/pyunits.mol), - 'G': (301.2961, pyunits.J/pyunits.mol/pyunits.K), - 'H': (-735.1288, pyunits.kJ/pyunits.mol)}, - "cp_mol_liq_comp_coeff": { - '1': (-202.695, pyunits.J/pyunits.kmol/pyunits.K), # [6] - '2': (2.9994, pyunits.J/pyunits.kmol/pyunits.K**2), - '3': (-9.239e-3, pyunits.J/pyunits.kmol/pyunits.K**3), - '4': (1.0113e-5, pyunits.J/pyunits.kmol/pyunits.K**4), - '5': (0, pyunits.J/pyunits.kmol/pyunits.K**5)}, - "enth_mol_form_liq_comp_ref": ( - -868.73e3, pyunits.J/pyunits.mol), # [4] - "enth_mol_form_vap_comp_ref": ( - -801.14e3, pyunits.J/pyunits.mol), # [4] - "pressure_sat_comp_coeff": {'A': (-18.122, None), # [5] - 'B': (10.596, None), - 'C': (-18.908, None), - 'D': (-32.728, None)}}}, - 'ethylene_glycol': - {"type": Component, - "elemental_composition": {"C": 2, "H": 6, "O": 2}, - "dens_mol_liq_comp": Perrys, - "enth_mol_liq_comp": Perrys, - "enth_mol_ig_comp": RPP4, - "pressure_sat_comp": RPP4, - "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, - "parameter_data": { - "mw": (62.069E-3, pyunits.kg/pyunits.mol), # [1] - "pressure_crit": (77e5, pyunits.Pa), # [1] - "temperature_crit": (645, pyunits.K), # [1] - "dens_mol_liq_comp_coeff": { - 'eqn_type': 1, - '1': (1.315, pyunits.kmol*pyunits.m**-3), # [2] pg. 2-98 - '2': (0.25125, None), - '3': (720, pyunits.K), - '4': (0.21868, None)}, - "cp_mol_ig_comp_coeff": { - 'A': (3.570E1, pyunits.J/pyunits.mol/pyunits.K), # [1] - 'B': (2.483E-1, pyunits.J/pyunits.mol/pyunits.K**2), - 'C': (-1.497E-4, pyunits.J/pyunits.mol/pyunits.K**3), - 'D': (3.010E-8, pyunits.J/pyunits.mol/pyunits.K**4)}, - "cp_mol_liq_comp_coeff": { - '1': (3.5540E1, pyunits.J/pyunits.kmol/pyunits.K), # [2] - '2': (4.3678E-1, pyunits.J/pyunits.kmol/pyunits.K**2), - '3': (-1.8486E-4, pyunits.J/pyunits.kmol/pyunits.K**3), - '4': (0, pyunits.J/pyunits.kmol/pyunits.K**4), - '5': (0, pyunits.J/pyunits.kmol/pyunits.K**5)}, - "enth_mol_form_liq_comp_ref": ( - -455.24e3, pyunits.J/pyunits.mol), # [3] - "enth_mol_form_vap_comp_ref": ( - -389.37e3, pyunits.J/pyunits.mol), # [3] - "pressure_sat_comp_coeff": {'A': (13.6299, None), # [1] - 'B': (6022.18, None), - 'C': (-28.25, None), - 'D': (0, None)}}}}, - + "ethylene_oxide": { + "type": Component, + "elemental_composition": {"C": 2, "H": 4, "O": 1}, + "dens_mol_liq_comp": Perrys, + "enth_mol_liq_comp": Perrys, + "enth_mol_ig_comp": RPP4, + "pressure_sat_comp": RPP4, + "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, + "parameter_data": { + "mw": (44.054e-3, pyunits.kg / pyunits.mol), # [1] pg. 676 + "pressure_crit": (71.9e5, pyunits.Pa), # [1] pg. 676 + "temperature_crit": (469, pyunits.K), # [1] pg. 676 + "dens_mol_liq_comp_coeff": { # [2] pg. 2-97 + "eqn_type": 1, + "1": (1.836, pyunits.kmol * pyunits.m**-3), + "2": (0.26024, None), + "3": (469.15, pyunits.K), + "4": (0.2696, None), + }, + "cp_mol_ig_comp_coeff": { # [1] pg. 677 + "A": (-7.519e0, pyunits.J / pyunits.mol / pyunits.K), + "B": (2.222e-1, pyunits.J / pyunits.mol / pyunits.K**2), + "C": (-1.256e-4, pyunits.J / pyunits.mol / pyunits.K**3), + "D": (2.592e-8, pyunits.J / pyunits.mol / pyunits.K**4), + }, + "cp_mol_liq_comp_coeff": { # [2] pg. 2-173 + "1": (1.4471e5, pyunits.J / pyunits.kmol / pyunits.K), + "2": (-7.5887e2, pyunits.J / pyunits.kmol / pyunits.K**2), + "3": (2.8261, pyunits.J / pyunits.kmol / pyunits.K**3), + "4": (-3.0640e-3, pyunits.J / pyunits.kmol / pyunits.K**4), + "5": (0, pyunits.J / pyunits.kmol / pyunits.K**5), + }, + "enth_mol_form_liq_comp_ref": ( + -95.7e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + "enth_mol_form_vap_comp_ref": ( + -52.64e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + "pressure_sat_comp_coeff": { + "A": (-6.56234, None), # [1] pg. 678 + "B": (0.42696, None), + "C": (-1.25638, None), + "D": (-3.18133, None), + }, + }, + }, + "water": { + "type": Component, + "elemental_composition": {"H": 2, "O": 1}, + "dens_mol_liq_comp": Perrys, + "enth_mol_liq_comp": Perrys, + "enth_mol_ig_comp": RPP4, + "pressure_sat_comp": RPP4, + "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, + "parameter_data": { + "mw": (18.015e-3, pyunits.kg / pyunits.mol), # [1] pg. 667 + "pressure_crit": (221.2e5, pyunits.Pa), # [1] pg. 667 + "temperature_crit": (647.3, pyunits.K), # [1] pg. 667 + "dens_mol_liq_comp_coeff": { # [2] pg. 2-98 + "eqn_type": 1, + "1": (5.459, pyunits.kmol * pyunits.m**-3), + "2": (0.30542, None), + "3": (647.13, pyunits.K), + "4": (0.081, None), + }, + "cp_mol_ig_comp_coeff": { # [1] pg. 668 + "A": (3.224e1, pyunits.J / pyunits.mol / pyunits.K), + "B": (1.924e-3, pyunits.J / pyunits.mol / pyunits.K**2), + "C": (1.055e-5, pyunits.J / pyunits.mol / pyunits.K**3), + "D": (-3.596e-9, pyunits.J / pyunits.mol / pyunits.K**4), + }, + "cp_mol_liq_comp_coeff": { # [2] pg. 2-174 + "1": (2.7637e5, pyunits.J / pyunits.kmol / pyunits.K), + "2": (-2.0901e3, pyunits.J / pyunits.kmol / pyunits.K**2), + "3": (8.1250, pyunits.J / pyunits.kmol / pyunits.K**3), + "4": (-1.4116e-2, pyunits.J / pyunits.kmol / pyunits.K**4), + "5": (9.3701e-6, pyunits.J / pyunits.kmol / pyunits.K**5), + }, + "enth_mol_form_liq_comp_ref": ( + -285.830e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + "enth_mol_form_vap_comp_ref": ( + -241.826e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + "pressure_sat_comp_coeff": { + "A": (-7.76451, None), # [1] pg. 669 + "B": (1.45838, None), + "C": (-2.77580, None), + "D": (-1.23303, None), + }, + }, + }, + "sulfuric_acid": { + "type": Component, + "elemental_composition": {"H": 2, "S": 1, "O": 4}, + "dens_mol_liq_comp": Perrys, # fitted to this equation form + "enth_mol_liq_comp": Perrys, # fitted to this equation form + "enth_mol_ig_comp": NIST, + "pressure_sat_comp": RPP4, # fitted to this equation form + "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, + "parameter_data": { + "mw": (98.08e-3, pyunits.kg / pyunits.mol), # [4] 5/20/24 + "pressure_crit": (129.4262e5, pyunits.Pa), # [4] 5/20/2024 + "temperature_crit": (590.76, pyunits.K), # [4] 5/202/24 + "dens_mol_liq_comp_coeff": { + "eqn_type": 2, # [5] pg. 15-41 regressed from 100% H2SO4 density data + "1": (23.669, pyunits.kmol / pyunits.m**3), + "2": (-2.5307e-2, pyunits.kmol / pyunits.m**3 / pyunits.K), + "3": (3.3523e-5, pyunits.kmol / pyunits.m**3 / pyunits.K**2), + "4": (-1.8538e-8, pyunits.kmol / pyunits.m**3 / pyunits.K**3), + }, + "cp_mol_ig_comp_coeff": { # [3] valid on 298-1200 K, updated 5/10/2024 + "A": (47.28924, pyunits.J / pyunits.mol / pyunits.K), + "B": (190.3314, pyunits.J / pyunits.mol / pyunits.K / pyunits.kK), + "C": ( + -148.1299, + pyunits.J / pyunits.mol / pyunits.K / pyunits.kK**2, + ), + "D": ( + 43.86631, + pyunits.J / pyunits.mol / pyunits.K / pyunits.kK**3, + ), + "E": ( + -0.740016, + pyunits.J / pyunits.mol / pyunits.K / pyunits.kK**-2, + ), + "F": (-758.9525, pyunits.kJ / pyunits.mol), + "G": (301.2961, pyunits.J / pyunits.mol / pyunits.K), + "H": (-735.1288, pyunits.kJ / pyunits.mol), + }, + "cp_mol_liq_comp_coeff": { # [6] pg. 1189 regressed from x1=1 and Cp/R for all T values + "1": (-202.695, pyunits.J / pyunits.kmol / pyunits.K), + "2": (2.9994, pyunits.J / pyunits.kmol / pyunits.K**2), + "3": (-9.239e-3, pyunits.J / pyunits.kmol / pyunits.K**3), + "4": (1.0113e-5, pyunits.J / pyunits.kmol / pyunits.K**4), + "5": (0, pyunits.J / pyunits.kmol / pyunits.K**5), + }, + "enth_mol_form_liq_comp_ref": ( + -810.4097e3, + pyunits.J / pyunits.mol, + ), # [6] Table 4 pg. 1183 + "enth_mol_form_vap_comp_ref": ( + -735.13e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + "pressure_sat_comp_coeff": { + "A": (-18.122, None), # [5] data for regression on pg. 6-122 + "B": (10.596, None), + "C": (-18.908, None), + "D": (-32.728, None), + }, + }, + }, + "ethylene_glycol": { + "type": Component, + "elemental_composition": {"C": 2, "H": 6, "O": 2}, + "dens_mol_liq_comp": Perrys, + "enth_mol_liq_comp": Perrys, + "enth_mol_ig_comp": RPP4, + "pressure_sat_comp": RPP4, + "phase_equilibrium_form": {("Vap", "Liq"): fugacity}, + "parameter_data": { + "mw": (62.069e-3, pyunits.kg / pyunits.mol), # [1] pg. 676 + "pressure_crit": (77e5, pyunits.Pa), # [1] pg. 676 + "temperature_crit": (645, pyunits.K), # [1] pg. 676 + "dens_mol_liq_comp_coeff": { # [2] pg. 2-95 + "eqn_type": 1, + "1": (1.3151, pyunits.kmol * pyunits.m**-3), + "2": (0.25125, None), + "3": (719.7, pyunits.K), + "4": (0.2187, None), + }, + "cp_mol_ig_comp_coeff": { # [1] pg. 677 + "A": (3.570e1, pyunits.J / pyunits.mol / pyunits.K), + "B": (2.483e-1, pyunits.J / pyunits.mol / pyunits.K**2), + "C": (-1.497e-4, pyunits.J / pyunits.mol / pyunits.K**3), + "D": (3.010e-8, pyunits.J / pyunits.mol / pyunits.K**4), + }, + "cp_mol_liq_comp_coeff": { # [2] pg. 2-171 + "1": (3.5540e4, pyunits.J / pyunits.kmol / pyunits.K), + "2": (4.3678e2, pyunits.J / pyunits.kmol / pyunits.K**2), + "3": (-1.8486e-1, pyunits.J / pyunits.kmol / pyunits.K**3), + "4": (0, pyunits.J / pyunits.kmol / pyunits.K**4), + "5": (0, pyunits.J / pyunits.kmol / pyunits.K**5), + }, + "enth_mol_form_liq_comp_ref": ( + -460.0e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + "enth_mol_form_vap_comp_ref": ( + -394.4e3, + pyunits.J / pyunits.mol, + ), # [3] updated 5/10/24 + # [1] pg. 678 pressure sat coef values for alternative equation form + # ln Pvp = A - B/(T + C) with A = 13.6299, B = 6022.18, C = -28.25 + # reformulated for generic property supported form + # ln Pvp = [(1 - x)^-1 * (A*x + B*x^1.5 + C*x^3 + D*x^6)] * Pc where x = 1 - T/Tc + "pressure_sat_comp_coeff": { + "A": (-16.4022, None), + "B": (10.0100, None), + "C": (-6.5216, None), + "D": (-11.1182, None), + }, + }, + }, + }, # Specifying phases - "phases": {'Liq': {"type": LiquidPhase, - "equation_of_state": Ideal}}, - + "phases": {"Liq": {"type": LiquidPhase, "equation_of_state": Ideal}}, # Set base units of measurement - "base_units": {"time": pyunits.s, - "length": pyunits.m, - "mass": pyunits.kg, - "amount": pyunits.mol, - "temperature": pyunits.K}, - + "base_units": { + "time": pyunits.s, + "length": pyunits.m, + "mass": pyunits.kg, + "amount": pyunits.mol, + "temperature": pyunits.K, + }, # Specifying state definition "state_definition": FpcTP, - "state_bounds": {"flow_mol_phase_comp": (0, 100, 1000, - pyunits.mol/pyunits.s), - "temperature": (273.15, 298.15, 450, pyunits.K), - "pressure": (5e4, 1e5, 1e6, pyunits.Pa)}, + "state_bounds": { + "flow_mol_phase_comp": (0, 100, 1000, pyunits.mol / pyunits.s), + "temperature": (273.15, 298.15, 450, pyunits.K), + "pressure": (5e4, 1e5, 1e6, pyunits.Pa), + }, "pressure_ref": (1e5, pyunits.Pa), - "temperature_ref": (298.15, pyunits.K)} + "temperature_ref": (298.15, pyunits.K), +} diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_doc.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_doc.ipynb index 0a3935a7..bafa95cf 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_doc.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_doc.ipynb @@ -1,1352 +1,1098 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "tags": [ - "header", - "hide-cell" - ] - }, - "outputs": [], - "source": [ - "###############################################################################\n", - "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", - "# Framework (IDAES IP) was produced under the DOE Institute for the\n", - "# Design of Advanced Energy Systems (IDAES).\n", - "#\n", - "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", - "# University of California, through Lawrence Berkeley National Laboratory,\n", - "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", - "# University, West Virginia University Research Corporation, et al.\n", - "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", - "# for full copyright and license information.\n", - "###############################################################################" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Flowsheet Equilibrium Reactor Simulation and Optimization of Steam Methane Reforming\n", - "Maintainer: Brandon Paul \n", - "Author: Brandon Paul \n", - "Updated: 2023-06-01 \n", - "\n", - "\n", - "## Learning Outcomes\n", - "\n", - "\n", - "- Call and implement the IDAES EquilibriumReactor unit model\n", - "- Construct a steady-state flowsheet using the IDAES unit model library\n", - "- Connecting unit models in a flowsheet using Arcs\n", - "- Fomulate and solve an optimization problem\n", - " - Defining an objective function\n", - " - Setting variable bounds\n", - " - Adding additional constraints \n", - "\n", - "\n", - "## Problem Statement\n", - "\n", - "This example is adapted from S.Z. Abbas, V. Dupont, T. Mahmud, Kinetics study and modelling of steam methane reforming process over a NiO/Al2O3 catalyst in an adiabatic packed bed reactor. Int. J. Hydrogen Energy, 42 (2017), pp. 2889-2903\n", - "\n", - "Steam methane reforming (SMR) is one of the most common pathways for hydrogen production, taking advantage of chemical equilibria in natural gas systems. The process is typically done in two steps: methane reformation at a high temperature to partially oxidize methane, and water gas shift at a low temperature to complete the oxidation reaction:\n", - "\n", - "**CH4 + H2O → CO + 3H2** \n", - "**CO + H2O → CO2 + H2**\n", - "\n", - "This reaction is often carried out in two separate reactors to allow for different reaction temperatures and pressures; in this example, we will minimize operating cost for a single reactor.\n", - "\n", - "The flowsheet that we will be using for this module is shown below with the stream conditions. We will be processing natural gas and steam feeds of fixed composition to produce hydrogen. As shown in the flowsheet, the process consists of a mixer M101 for the two inlet streams, a compressor to compress the feed to the reaction pressure, a heater H101 to heat the feed to the reaction temperature, and a EquilibriumReactor unit R101. We will use thermodynamic properties from the Peng-Robinson equation of state for this flowsheet.\n", - "\n", - "The state variables chosen for the property package are **total molar flows of each stream, temperature of each stream and pressure of each stream, and mole fractions of each component in each stream**. The components considered are: **CH4, H2O, CO, CO2, and H2** and the process occurs in vapor phase only. Therefore, every stream has 1 flow variable, 5 mole fraction variables, 1 temperature and 1 pressure variable. \n", - "\n", - "![](msr_flowsheet.png)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Pyomo and IDAES Components\n", - "\n", - "\n", - "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages. Let us first import the following components from Pyomo:\n", - "- Constraint (to write constraints)\n", - "- Var (to declare variables)\n", - "- ConcreteModel (to create the concrete model object)\n", - "- Expression (to evaluate values as a function of variables defined in the model)\n", - "- Objective (to define an objective function for optimization)\n", - "- TransformationFactory (to apply certain transformations)\n", - "- Arc (to connect two unit models)\n", - "\n", - "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\n", - "\n", - "From IDAES, we will be needing the `FlowsheetBlock` and the following unit models:\n", - "- Feed\n", - "- Mixer\n", - "- Compressor\n", - "- Heater\n", - "- EquilibriumReactor\n", - "- Product\n", - "\n", - "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom, tools for model expressions and calling variable values, and built-in functions to define property packages, add unit containers to objects and define our initialization scheme.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from pyomo.environ import (\n", - " Constraint,\n", - " Var,\n", - " ConcreteModel,\n", - " Expression,\n", - " Objective,\n", - " TransformationFactory,\n", - " value,\n", - " units as pyunits,\n", - ")\n", - "from pyomo.network import Arc\n", - "\n", - "from idaes.core import FlowsheetBlock\n", - "from idaes.models.properties.modular_properties.base.generic_property import (\n", - " GenericParameterBlock,\n", - ")\n", - "from idaes.models.properties.modular_properties.base.generic_reaction import (\n", - " GenericReactionParameterBlock,\n", - ")\n", - "from idaes.models.unit_models import (\n", - " Feed,\n", - " Mixer,\n", - " Compressor,\n", - " Heater,\n", - " EquilibriumReactor,\n", - " Product,\n", - ")\n", - "\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.util.initialization import propagate_state" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Thermophysical and Reaction Packages\n", - "\n", - "The final step is to import the thermophysical and reaction packages. We will import natural gas properties from an existing IDAES module, and reaction properties from a custom module to describe equilibrium behavior. These configuration dictionaries provide parameter data that we will pass to the Modular Property Framework.\n", - "\n", - "The reaction package here assumes all reactions reach chemical equilibrium at the given conditions. \n", - "\n", - "${K_{eq}^{MSR}} = \\exp\\left(\\frac {-26830} {T} + 30.114\\right)$, ${K_{eq}^{WGS}} = \\exp\\left(\\frac {4400} {T} - 4.036\\right)$ with the reactor temperature $T$ in K. \n", - "The total reaction equilibrium constant is given by $K_{eq} = {K_{eq}^{MSR}}{K_{eq}^{WGS}}$.\n", - "\n", - "The correlations are taken from the following literature: \n", - "\n", - "Int. J. Hydrogen Energy, 42 (2017), pp. 2889-2903\n", - "\n", - "### Determining $k_{eq}^{ref}$\n", - "\n", - "As part of the parameter dictionary, users may define equilibrium reactions using a constant coefficient or built-in correlations for van't Hoff and Gibbs formulations. Using the literature correlations above for $k_{eq}$, we can easily calculate the necessary parameters to use the van't Hoff equilibrium constant form:\n", - "\n", - "For an empirical correlation $ln(k_{eq}) = f(T)$ for a catalyst (reaction) temperature $T$, we obtain $k_{eq}^{ref} = \\exp\\left({f(T_{eq}^{ref})}\\right)$. From the paper, we obtain a reference catalyst temperature of 973.15 K and reaction energies for the two reaction steps; these values exist in the reaction property parameter module in this same directory.\n", - "\n", - "These calculations are contained within the property, reaction and unit model packages, and do not need to be entered into the flowsheet. More information on property estimation may be found in the IDAES documentation on [Parameter Estimation](https://idaes-pse.readthedocs.io/en/stable/how_to_guides/workflow/data_rec_parmest.html).\n", - "\n", - "Let us import the following modules:\n", - "- natural_gas_PR as get_prop (method to get configuration dictionary)\n", - "- msr_reaction as reaction_props (contains configuration dictionary)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop\n", - "import msr_reaction as reaction_props" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Constructing the Flowsheet\n", - "\n", - "We have now imported all the components, unit models, and property modules we need to construct a flowsheet. Let us create a `ConcreteModel` and add the flowsheet block. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "m = ConcreteModel()\n", - "m.fs = FlowsheetBlock(dynamic=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now need to add the property packages to the flowsheet. Unlike the basic [Flash unit model example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Tutorials/Basics/flash_unit_solution_testing_doc.md), where we only had a thermophysical property package, for this flowsheet we will also need to add a reaction property package. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) and [Modular Reaction Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-reaction-package-framework). The `get_prop` method for the natural gas property module automatically returns the correct dictionary using a component list argument. The `GenericParameterBlock` and `GenericReactionParameterBlock` methods build states blocks from passed parameter data; the reaction block unpacks using `**reaction_props.config_dict` to allow for optional or empty keyword arguments:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "thermo_props_config_dict = get_prop(components=[\"CH4\", \"H2O\", \"H2\", \"CO\", \"CO2\"])\n", - "m.fs.thermo_params = GenericParameterBlock(**thermo_props_config_dict)\n", - "m.fs.reaction_params = GenericReactionParameterBlock(\n", - " property_package=m.fs.thermo_params, **reaction_props.config_dict\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Unit Models\n", - "\n", - "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding a `Mixer`, a `Compressor`, a `Heater` and an `EquilibriumReactor`. Note that all unit models should be explicitly defined with a given property package. In addition to that, there are several arguments depending on the unit model, please refer to the documentation for more details on [IDAES Unit Models](https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/index.html). For example, the `Mixer` is given a `list` consisting of names to the two inlets. Note that the `Compressor` is a `PressureChanger` assuming compression operation and with a fixed isentropic compressor efficiency as the default thermodynamic behavior." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.fs.CH4 = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.H2O = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", - "m.fs.M101 = Mixer(\n", - " property_package=m.fs.thermo_params, inlet_list=[\"methane_feed\", \"steam_feed\"]\n", - ")\n", - "m.fs.H101 = Heater(\n", - " property_package=m.fs.thermo_params,\n", - " has_pressure_change=False,\n", - " has_phase_equilibrium=False,\n", - ")\n", - "m.fs.C101 = Compressor(property_package=m.fs.thermo_params)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "m.fs.R101 = EquilibriumReactor(\n", - " property_package=m.fs.thermo_params,\n", - " reaction_package=m.fs.reaction_params,\n", - " has_equilibrium_reactions=True,\n", - " has_rate_reactions=False,\n", - " has_heat_of_reaction=True,\n", - " has_heat_transfer=True,\n", - " has_pressure_change=False,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Connecting Unit Models Using Arcs\n", - "\n", - "We have now added all the unit models we need to the flowsheet. However, we have not yet specified how the units are to be connected. To do this, we will be using the `Arc` which is a Pyomo component that takes in two arguments: `source` and `destination`. Let us connect the outlet of the `Mixer` to the inlet of the `Compressor`, the outlet of the compressor `Compressor` to the inlet of the `Heater`, and the outlet of the `Heater` to the inlet of the `EquilibriumReactor`. Additionally, we will connect the `Feed` and `Product` blocks to the flowsheet:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.s01 = Arc(source=m.fs.CH4.outlet, destination=m.fs.M101.methane_feed)\n", - "m.fs.s02 = Arc(source=m.fs.H2O.outlet, destination=m.fs.M101.steam_feed)\n", - "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.C101.inlet)\n", - "m.fs.s04 = Arc(source=m.fs.C101.outlet, destination=m.fs.H101.inlet)\n", - "m.fs.s05 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", - "m.fs.s06 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We have now connected the unit model block using the arcs. However, we also need to link the state variables on connected ports. Pyomo provides a convenient method `TransformationFactory` to write these equality constraints for us between two ports:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "TransformationFactory(\"network.expand_arcs\").apply_to(m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Expressions to Compute Operating Costs\n", - "\n", - "In this section, we will add a few `Expressions` that allow us to evaluate the performance. `Expressions` provide a convenient way of calculating certain values that are a function of the variables defined in the model. For more details on `Expressions`, please refer to the [Pyomo Expression documentation](https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html).\n", - "\n", - "For this flowsheet, we are interested in computing hydrogen production in millions of pounds per year, as well as the total costs due to pressurizing, cooling, and heating utilities.\n", - "\n", - "Let us first add an `Expression` to convert the product flow from mol/s to MM lb/year of hydrogen. We see that the molecular weight exists in the thermophysical property package, so we may use that value for our calculations." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.hyd_prod = Expression(\n", - " expr=pyunits.convert(\n", - " m.fs.PROD.inlet.flow_mol[0]\n", - " * m.fs.PROD.inlet.mole_frac_comp[0, \"H2\"]\n", - " * m.fs.thermo_params.H2.mw, # MW defined in properties as kg/mol\n", - " to_units=pyunits.Mlb / pyunits.yr,\n", - " )\n", - ") # converting kg/s to MM lb/year" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, let us add expressions to compute the reactor cooling cost (\\\\$/s) assuming a cost of 2.12E-5 \\\\$/kW, the compression cost (\\\\$/s) assuming 1.2E-3 \\\\$/kW, and the heating utility cost (\\\\$/s) assuming 2.2E-4 \\\\$/kW. Note that the heat duty is in units of Watt (J/s). The total operating cost will be the sum of the costs, expressed in \\\\$/year assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.cooling_cost = Expression(\n", - " expr=2.12e-8 * (m.fs.R101.heat_duty[0])\n", - ") # the reaction is endothermic, so R101 duty is positive\n", - "m.fs.heating_cost = Expression(\n", - " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", - ") # the stream must be heated to T_rxn, so H101 duty is positive\n", - "m.fs.compression_cost = Expression(\n", - " expr=1.2e-6 * m.fs.C101.work_isentropic[0]\n", - ") # the stream must be pressurized, so the C101 work is positive\n", - "m.fs.operating_cost = Expression(\n", - " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost + m.fs.compression_cost))\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Feed Conditions\n", - "\n", - "Let us first check how many degrees of freedom exist for this flowsheet using the `degrees_of_freedom` tool we imported earlier. We expect each stream to have 8 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the compressor to have 2 (the pressure change and efficiency), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 1 (conversion). Therefore, we have 20 degrees of freedom to specify: temperature, pressure, flow and mole fractions of all five components on both streams; compressor pressure change and efficiency; outlet heater temperature; and reactor conversion.\n", - "\n", - "Although the model has eight degrees of freedom per stream, the mole fractions are not all independent and the physical system only has seven. Each `StateBlock` sets a flag `defined_state` based on any remaining degrees of freedom; if this flag is set to `False` a `Constraint` is written to ensure all mole fractions sum to one. However, a fully specified system with `defined_state` set to `True` will not create this constraint and it is the responsibility of the user to set physically meaningful values, i.e. that all mole fractions are nonnegative and sum to one. While not necessary in this example, the [Custom Thermophysical Property Package Example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Examples/Advanced/CustomProperties/custom_physical_property_packages_testing_doc.md) demonstrates adding a check before writing an additional constraint that may overspecify the system." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "20\n" - ] - } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will now be fixing the feed stream to the conditions shown in the flowsheet above. As mentioned in other tutorials, the IDAES framework expects a time index value for every referenced internal stream or unit variable, even in steady-state systems with a single time point $ t = 0 $ (`t = [0]` is the default when creating a `FlowsheetBlock` without passing a `time_set` argument). The non-present components in each stream are assigned a very small non-zero value to help with convergence and initializing. Based on the literature source, we will initialize our simulation with the following values:" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.CH4.outlet.mole_frac_comp[0, \"CH4\"].fix(1)\n", - "m.fs.CH4.outlet.mole_frac_comp[0, \"H2O\"].fix(1e-5)\n", - "m.fs.CH4.outlet.mole_frac_comp[0, \"H2\"].fix(1e-5)\n", - "m.fs.CH4.outlet.mole_frac_comp[0, \"CO\"].fix(1e-5)\n", - "m.fs.CH4.outlet.mole_frac_comp[0, \"CO2\"].fix(1e-5)\n", - "m.fs.CH4.outlet.flow_mol.fix(75 * pyunits.mol / pyunits.s)\n", - "m.fs.CH4.outlet.temperature.fix(298.15 * pyunits.K)\n", - "m.fs.CH4.outlet.pressure.fix(1e5 * pyunits.Pa)\n", - "\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"CH4\"].fix(1e-5)\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"H2O\"].fix(1)\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"H2\"].fix(1e-5)\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"CO\"].fix(1e-5)\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"CO2\"].fix(1e-5)\n", - "m.fs.H2O.outlet.flow_mol.fix(234 * pyunits.mol / pyunits.s)\n", - "m.fs.H2O.outlet.temperature.fix(373.15 * pyunits.K)\n", - "m.fs.H2O.outlet.pressure.fix(1e5 * pyunits.Pa)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Unit Model Specifications\n", - "\n", - "Now that we have fixed our inlet feed conditions, we will now be fixing the operating conditions for the unit models in the flowsheet. For the initial problem, let us fix the compressor outlet pressure to 2 bar for now, the efficiency to 0.90 (a common assumption for compressor units), and the heater outlet temperature to 500 K. We will unfix these values later to optimize the flowsheet." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.C101.outlet.pressure.fix(pyunits.convert(2 * pyunits.bar, to_units=pyunits.Pa))\n", - "m.fs.C101.efficiency_isentropic.fix(0.90)\n", - "m.fs.H101.outlet.temperature.fix(500 * pyunits.K)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `EquilibriumReactor` unit model calculates the amount of product and reactant based on the calculated equilibrium constant; therefore, we will specify a desired conversion and let the solver determine the reactor duty and heat transfer. For convenience, we will define the reactor conversion as the amount of methane that is converted." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.R101.conversion = Var(\n", - " initialize=0.80, bounds=(0, 1), units=pyunits.dimensionless\n", - ") # fraction\n", - "\n", - "m.fs.R101.conv_constraint = Constraint(\n", - " expr=m.fs.R101.conversion\n", - " * m.fs.R101.inlet.flow_mol[0]\n", - " * m.fs.R101.inlet.mole_frac_comp[0, \"CH4\"]\n", - " == (\n", - " m.fs.R101.inlet.flow_mol[0] * m.fs.R101.inlet.mole_frac_comp[0, \"CH4\"]\n", - " - m.fs.R101.outlet.flow_mol[0] * m.fs.R101.outlet.mole_frac_comp[0, \"CH4\"]\n", - " )\n", - ")\n", - "\n", - "m.fs.R101.conversion.fix(0.80)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "0\n" - ] - } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we need to initialize each unit operation in sequence to solve the flowsheet. As in best practice, unit operations are initialized or solved, and outlet properties are propagated to connected inlet streams via arc definitions as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [ + "header", + "hide-cell" + ] + }, + "outputs": [], + "source": [ + "###############################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "###############################################################################" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.CH4.properties: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "# Flowsheet Equilibrium Reactor Simulation and Optimization of Steam Methane Reforming\n", + "Maintainer: Brandon Paul \n", + "Author: Brandon Paul \n", + "Updated: 2023-06-01 \n", + "\n", + "\n", + "## Learning Outcomes\n", + "\n", + "\n", + "- Call and implement the IDAES EquilibriumReactor unit model\n", + "- Construct a steady-state flowsheet using the IDAES unit model library\n", + "- Connecting unit models in a flowsheet using Arcs\n", + "- Fomulate and solve an optimization problem\n", + " - Defining an objective function\n", + " - Setting variable bounds\n", + " - Adding additional constraints \n", + "\n", + "\n", + "## Problem Statement\n", + "\n", + "This example is adapted from S.Z. Abbas, V. Dupont, T. Mahmud, Kinetics study and modelling of steam methane reforming process over a NiO/Al2O3 catalyst in an adiabatic packed bed reactor. Int. J. Hydrogen Energy, 42 (2017), pp. 2889-2903\n", + "\n", + "Steam methane reforming (SMR) is one of the most common pathways for hydrogen production, taking advantage of chemical equilibria in natural gas systems. The process is typically done in two steps: methane reformation at a high temperature to partially oxidize methane, and water gas shift at a low temperature to complete the oxidation reaction:\n", + "\n", + "**CH4 + H2O \u2192 CO + 3H2** \n", + "**CO + H2O \u2192 CO2 + H2**\n", + "\n", + "This reaction is often carried out in two separate reactors to allow for different reaction temperatures and pressures; in this example, we will minimize operating cost for a single reactor.\n", + "\n", + "The flowsheet that we will be using for this module is shown below with the stream conditions. We will be processing natural gas and steam feeds of fixed composition to produce hydrogen. As shown in the flowsheet, the process consists of a mixer M101 for the two inlet streams, a compressor to compress the feed to the reaction pressure, a heater H101 to heat the feed to the reaction temperature, and a EquilibriumReactor unit R101. We will use thermodynamic properties from the Peng-Robinson equation of state for this flowsheet.\n", + "\n", + "The state variables chosen for the property package are **total molar flows of each stream, temperature of each stream and pressure of each stream, and mole fractions of each component in each stream**. The components considered are: **CH4, H2O, CO, CO2, and H2** and the process occurs in vapor phase only. Therefore, every stream has 1 flow variable, 5 mole fraction variables, 1 temperature and 1 pressure variable. \n", + "\n", + "![](msr_flowsheet.png)\n" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.CH4.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Pyomo and IDAES Components\n", + "\n", + "\n", + "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages. Let us first import the following components from Pyomo:\n", + "- Constraint (to write constraints)\n", + "- Var (to declare variables)\n", + "- ConcreteModel (to create the concrete model object)\n", + "- Expression (to evaluate values as a function of variables defined in the model)\n", + "- Objective (to define an objective function for optimization)\n", + "- TransformationFactory (to apply certain transformations)\n", + "- Arc (to connect two unit models)\n", + "\n", + "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\n", + "\n", + "From IDAES, we will be needing the `FlowsheetBlock` and the following unit models:\n", + "- Feed\n", + "- Mixer\n", + "- Compressor\n", + "- Heater\n", + "- EquilibriumReactor\n", + "- Product\n", + "\n", + "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom, tools for model expressions and calling variable values, and built-in functions to define property packages, add unit containers to objects and define our initialization scheme.\n" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.CH4.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from pyomo.environ import (\n", + " Constraint,\n", + " Var,\n", + " ConcreteModel,\n", + " Expression,\n", + " Objective,\n", + " TransformationFactory,\n", + " value,\n", + " units as pyunits,\n", + ")\n", + "from pyomo.network import Arc\n", + "\n", + "from idaes.core import FlowsheetBlock\n", + "from idaes.models.properties.modular_properties.base.generic_property import (\n", + " GenericParameterBlock,\n", + ")\n", + "from idaes.models.properties.modular_properties.base.generic_reaction import (\n", + " GenericReactionParameterBlock,\n", + ")\n", + "from idaes.models.unit_models import (\n", + " Feed,\n", + " Mixer,\n", + " Compressor,\n", + " Heater,\n", + " EquilibriumReactor,\n", + " Product,\n", + ")\n", + "\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.util.initialization import propagate_state" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.CH4: Initialization Complete.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Thermophysical and Reaction Packages\n", + "\n", + "The final step is to import the thermophysical and reaction packages. We will import natural gas properties from an existing IDAES module, and reaction properties from a custom module to describe equilibrium behavior. These configuration dictionaries provide parameter data that we will pass to the Modular Property Framework.\n", + "\n", + "The reaction package here assumes all reactions reach chemical equilibrium at the given conditions. \n", + "\n", + "${K_{eq}^{MSR}} = \\exp\\left(\\frac {-26830} {T} + 30.114\\right)$, ${K_{eq}^{WGS}} = \\exp\\left(\\frac {4400} {T} - 4.036\\right)$ with the reactor temperature $T$ in K. \n", + "The total reaction equilibrium constant is given by $K_{eq} = {K_{eq}^{MSR}}{K_{eq}^{WGS}}$.\n", + "\n", + "The correlations are taken from the following literature: \n", + "\n", + "Int. J. Hydrogen Energy, 42 (2017), pp. 2889-2903\n", + "\n", + "### Determining $k_{eq}^{ref}$\n", + "\n", + "As part of the parameter dictionary, users may define equilibrium reactions using a constant coefficient or built-in correlations for van't Hoff and Gibbs formulations. Using the literature correlations above for $k_{eq}$, we can easily calculate the necessary parameters to use the van't Hoff equilibrium constant form:\n", + "\n", + "For an empirical correlation $ln(k_{eq}) = f(T)$ for a catalyst (reaction) temperature $T$, we obtain $k_{eq}^{ref} = \\exp\\left({f(T_{eq}^{ref})}\\right)$. From the paper, we obtain a reference catalyst temperature of 973.15 K and reaction energies for the two reaction steps; these values exist in the reaction property parameter module in this same directory.\n", + "\n", + "These calculations are contained within the property, reaction and unit model packages, and do not need to be entered into the flowsheet. More information on property estimation may be found in the IDAES documentation on [Parameter Estimation](https://idaes-pse.readthedocs.io/en/stable/how_to_guides/workflow/data_rec_parmest.html).\n", + "\n", + "Let us import the following modules:\n", + "- natural_gas_PR as get_prop (method to get configuration dictionary)\n", + "- msr_reaction as reaction_props (contains configuration dictionary)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.H2O.properties: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop\n", + "import msr_reaction as reaction_props" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.H2O.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constructing the Flowsheet\n", + "\n", + "We have now imported all the components, unit models, and property modules we need to construct a flowsheet. Let us create a `ConcreteModel` and add the flowsheet block. " + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.H2O.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "m = ConcreteModel()\n", + "m.fs = FlowsheetBlock(dynamic=False)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.H2O: Initialization Complete.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to add the property packages to the flowsheet. Unlike the basic [Flash unit model example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Tutorials/Basics/flash_unit_solution_testing_doc.md), where we only had a thermophysical property package, for this flowsheet we will also need to add a reaction property package. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) and [Modular Reaction Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-reaction-package-framework). The `get_prop` method for the natural gas property module automatically returns the correct dictionary using a component list argument. The `GenericParameterBlock` and `GenericReactionParameterBlock` methods build states blocks from passed parameter data; the reaction block unpacks using `**reaction_props.config_dict` to allow for optional or empty keyword arguments:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.M101.methane_feed_state: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 5, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "thermo_props_config_dict = get_prop(components=[\"CH4\", \"H2O\", \"H2\", \"CO\", \"CO2\"])\n", + "m.fs.thermo_params = GenericParameterBlock(**thermo_props_config_dict)\n", + "m.fs.reaction_params = GenericReactionParameterBlock(\n", + " property_package=m.fs.thermo_params, **reaction_props.config_dict\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.M101.methane_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Unit Models\n", + "\n", + "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding a `Mixer`, a `Compressor`, a `Heater` and an `EquilibriumReactor`. Note that all unit models should be explicitly defined with a given property package. In addition to that, there are several arguments depending on the unit model, please refer to the documentation for more details on [IDAES Unit Models](https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/index.html). For example, the `Mixer` is given a `list` consisting of names to the two inlets. Note that the `Compressor` is a `PressureChanger` assuming compression operation and with a fixed isentropic compressor efficiency as the default thermodynamic behavior." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.M101.steam_feed_state: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "m.fs.CH4 = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.H2O = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", + "m.fs.M101 = Mixer(\n", + " property_package=m.fs.thermo_params, inlet_list=[\"methane_feed\", \"steam_feed\"]\n", + ")\n", + "m.fs.H101 = Heater(\n", + " property_package=m.fs.thermo_params,\n", + " has_pressure_change=False,\n", + " has_phase_equilibrium=False,\n", + ")\n", + "m.fs.C101 = Compressor(property_package=m.fs.thermo_params)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.M101.steam_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101 = EquilibriumReactor(\n", + " property_package=m.fs.thermo_params,\n", + " reaction_package=m.fs.reaction_params,\n", + " has_equilibrium_reactions=True,\n", + " has_rate_reactions=False,\n", + " has_heat_of_reaction=True,\n", + " has_heat_transfer=True,\n", + " has_pressure_change=False,\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Connecting Unit Models Using Arcs\n", + "\n", + "We have now added all the unit models we need to the flowsheet. However, we have not yet specified how the units are to be connected. To do this, we will be using the `Arc` which is a Pyomo component that takes in two arguments: `source` and `destination`. Let us connect the outlet of the `Mixer` to the inlet of the `Compressor`, the outlet of the compressor `Compressor` to the inlet of the `Heater`, and the outlet of the `Heater` to the inlet of the `EquilibriumReactor`. Additionally, we will connect the `Feed` and `Product` blocks to the flowsheet:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.s01 = Arc(source=m.fs.CH4.outlet, destination=m.fs.M101.methane_feed)\n", + "m.fs.s02 = Arc(source=m.fs.H2O.outlet, destination=m.fs.M101.steam_feed)\n", + "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.C101.inlet)\n", + "m.fs.s04 = Arc(source=m.fs.C101.outlet, destination=m.fs.H101.inlet)\n", + "m.fs.s05 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", + "m.fs.s06 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have now connected the unit model block using the arcs. However, we also need to link the state variables on connected ports. Pyomo provides a convenient method `TransformationFactory` to write these equality constraints for us between two ports:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "TransformationFactory(\"network.expand_arcs\").apply_to(m)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.C101.control_volume.properties_in: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Expressions to Compute Operating Costs\n", + "\n", + "In this section, we will add a few `Expressions` that allow us to evaluate the performance. `Expressions` provide a convenient way of calculating certain values that are a function of the variables defined in the model. For more details on `Expressions`, please refer to the [Pyomo Expression documentation](https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html).\n", + "\n", + "For this flowsheet, we are interested in computing hydrogen production in millions of pounds per year, as well as the total costs due to pressurizing, cooling, and heating utilities.\n", + "\n", + "Let us first add an `Expression` to convert the product flow from mol/s to MM lb/year of hydrogen. We see that the molecular weight exists in the thermophysical property package, so we may use that value for our calculations." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.C101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.hyd_prod = Expression(\n", + " expr=pyunits.convert(\n", + " m.fs.PROD.inlet.flow_mol[0]\n", + " * m.fs.PROD.inlet.mole_frac_comp[0, \"H2\"]\n", + " * m.fs.thermo_params.H2.mw, # MW defined in properties as kg/mol\n", + " to_units=pyunits.Mlb / pyunits.yr,\n", + " )\n", + ") # converting kg/s to MM lb/year" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.C101.control_volume.properties_out: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let us add expressions to compute the reactor cooling cost (\\\\$/s) assuming a cost of 2.12E-5 \\\\$/kW, the compression cost (\\\\$/s) assuming 1.2E-3 \\\\$/kW, and the heating utility cost (\\\\$/s) assuming 2.2E-4 \\\\$/kW. Note that the heat duty is in units of Watt (J/s). The total operating cost will be the sum of the costs, expressed in \\\\$/year assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.cooling_cost = Expression(\n", + " expr=2.12e-8 * (m.fs.R101.heat_duty[0])\n", + ") # the reaction is endothermic, so R101 duty is positive\n", + "m.fs.heating_cost = Expression(\n", + " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", + ") # the stream must be heated to T_rxn, so H101 duty is positive\n", + "m.fs.compression_cost = Expression(\n", + " expr=1.2e-6 * m.fs.C101.work_isentropic[0]\n", + ") # the stream must be pressurized, so the C101 work is positive\n", + "m.fs.operating_cost = Expression(\n", + " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost + m.fs.compression_cost))\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Feed Conditions\n", + "\n", + "Let us first check how many degrees of freedom exist for this flowsheet using the `degrees_of_freedom` tool we imported earlier. We expect each stream to have 8 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the compressor to have 2 (the pressure change and efficiency), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 1 (conversion). Therefore, we have 20 degrees of freedom to specify: temperature, pressure, flow and mole fractions of all five components on both streams; compressor pressure change and efficiency; outlet heater temperature; and reactor conversion.\n", + "\n", + "Although the model has eight degrees of freedom per stream, the mole fractions are not all independent and the physical system only has seven. Each `StateBlock` sets a flag `defined_state` based on any remaining degrees of freedom; if this flag is set to `False` a `Constraint` is written to ensure all mole fractions sum to one. However, a fully specified system with `defined_state` set to `True` will not create this constraint and it is the responsibility of the user to set physically meaningful values, i.e. that all mole fractions are nonnegative and sum to one. While not necessary in this example, the [Custom Thermophysical Property Package Example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Examples/Advanced/CustomProperties/custom_physical_property_packages_testing_doc.md) demonstrates adding a check before writing an additional constraint that may overspecify the system." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.C101.properties_isentropic: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 12, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "20\n" + ] + } + ], + "source": [ + "print(degrees_of_freedom(m))" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.C101.properties_isentropic: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now be fixing the feed stream to the conditions shown in the flowsheet above. As mentioned in other tutorials, the IDAES framework expects a time index value for every referenced internal stream or unit variable, even in steady-state systems with a single time point $ t = 0 $ (`t = [0]` is the default when creating a `FlowsheetBlock` without passing a `time_set` argument). The non-present components in each stream are assigned a very small non-zero value to help with convergence and initializing. Based on the literature source, we will initialize our simulation with the following values:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:20 [INFO] idaes.init.fs.C101.properties_isentropic: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.CH4.outlet.mole_frac_comp[0, \"CH4\"].fix(1)\n", + "m.fs.CH4.outlet.mole_frac_comp[0, \"H2O\"].fix(1e-5)\n", + "m.fs.CH4.outlet.mole_frac_comp[0, \"H2\"].fix(1e-5)\n", + "m.fs.CH4.outlet.mole_frac_comp[0, \"CO\"].fix(1e-5)\n", + "m.fs.CH4.outlet.mole_frac_comp[0, \"CO2\"].fix(1e-5)\n", + "m.fs.CH4.outlet.flow_mol.fix(75 * pyunits.mol / pyunits.s)\n", + "m.fs.CH4.outlet.temperature.fix(298.15 * pyunits.K)\n", + "m.fs.CH4.outlet.pressure.fix(1e5 * pyunits.Pa)\n", + "\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"CH4\"].fix(1e-5)\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"H2O\"].fix(1)\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"H2\"].fix(1e-5)\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"CO\"].fix(1e-5)\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"CO2\"].fix(1e-5)\n", + "m.fs.H2O.outlet.flow_mol.fix(234 * pyunits.mol / pyunits.s)\n", + "m.fs.H2O.outlet.temperature.fix(373.15 * pyunits.K)\n", + "m.fs.H2O.outlet.pressure.fix(1e5 * pyunits.Pa)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.C101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Unit Model Specifications\n", + "\n", + "Now that we have fixed our inlet feed conditions, we will now be fixing the operating conditions for the unit models in the flowsheet. For the initial problem, let us fix the compressor outlet pressure to 2 bar for now, the efficiency to 0.90 (a common assumption for compressor units), and the heater outlet temperature to 500 K. We will unfix these values later to optimize the flowsheet." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.C101.outlet.pressure.fix(pyunits.convert(2 * pyunits.bar, to_units=pyunits.Pa))\n", + "m.fs.C101.efficiency_isentropic.fix(0.90)\n", + "m.fs.H101.outlet.temperature.fix(500 * pyunits.K)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `EquilibriumReactor` unit model calculates the amount of product and reactant based on the calculated equilibrium constant; therefore, we will specify a desired conversion and let the solver determine the reactor duty and heat transfer. For convenience, we will define the reactor conversion as the amount of methane that is converted." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.conversion = Var(\n", + " initialize=0.80, bounds=(0, 1), units=pyunits.dimensionless\n", + ") # fraction\n", + "\n", + "m.fs.R101.conv_constraint = Constraint(\n", + " expr=m.fs.R101.conversion\n", + " * m.fs.R101.inlet.flow_mol[0]\n", + " * m.fs.R101.inlet.mole_frac_comp[0, \"CH4\"]\n", + " == (\n", + " m.fs.R101.inlet.flow_mol[0] * m.fs.R101.inlet.mole_frac_comp[0, \"CH4\"]\n", + " - m.fs.R101.outlet.flow_mol[0] * m.fs.R101.outlet.mole_frac_comp[0, \"CH4\"]\n", + " )\n", + ")\n", + "\n", + "m.fs.R101.conversion.fix(0.80)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n" - ] + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n" + ] + } + ], + "source": [ + "print(degrees_of_freedom(m))" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we need to initialize each unit operation in sequence to solve the flowsheet. As in best practice, unit operations are initialized or solved, and outlet properties are propagated to connected inlet streams via arc definitions as follows:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4.properties: Starting initialization\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4: Initialization Complete.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O.properties: Starting initialization\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O: Initialization Complete.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.M101.methane_feed_state: Starting initialization\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.M101.methane_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.M101.steam_feed_state: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.steam_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.properties_isentropic: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.properties_isentropic: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.properties_isentropic: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.C101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.reactions: Initialization Complete.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" + ] + } + ], + "source": [ + "# Initialize and solve each unit operation\n", + "m.fs.CH4.initialize()\n", + "propagate_state(arc=m.fs.s01)\n", + "\n", + "m.fs.H2O.initialize()\n", + "propagate_state(arc=m.fs.s02)\n", + "\n", + "m.fs.M101.initialize()\n", + "propagate_state(arc=m.fs.s03)\n", + "\n", + "m.fs.C101.initialize()\n", + "propagate_state(arc=m.fs.s04)\n", + "\n", + "m.fs.H101.initialize()\n", + "propagate_state(arc=m.fs.s05)\n", + "\n", + "m.fs.R101.initialize()\n", + "propagate_state(arc=m.fs.s06)\n", + "\n", + "m.fs.PROD.initialize()\n", + "\n", + "# set solver\n", + "solver = get_solver()" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 20, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 562\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 477\n", + "\n", + "Total number of variables............................: 204\n", + " variables with only lower bounds: 13\n", + " variables with lower and upper bounds: 174\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 204\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.49e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 1.35e+04 2.00e-01 -1.0 3.59e+00 - 9.90e-01 9.91e-01h 1\n", + " 2 0.0000000e+00 3.59e-04 9.99e+00 -1.0 3.56e+00 - 9.90e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 2.12e-08 8.98e+01 -1.0 2.91e-04 - 9.90e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 3\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.8421709430404007e-14 2.1187588572502136e-08\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.8421709430404007e-14 2.1187588572502136e-08\n", + "\n", + "\n", + "Number of objective function evaluations = 4\n", + "Number of objective gradient evaluations = 4\n", + "Number of equality constraint evaluations = 4\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 4\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 3\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.003\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], + "source": [ + "# Solve the model\n", + "results = solver.solve(m, tee=True)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyze the Results of the Square Problem\n", + "\n", + "\n", + "What is the total operating cost? " + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $45.933 million per year\n" + ] + } + ], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.R101.control_volume.reactions: Initialization Complete.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this operating cost, what conversion did we achieve of methane to hydrogen?" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n" - ] + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 2.7605e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 429.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.034965\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.31487\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.51029\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.049157\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.090717\n", + " Temperature kelvin 500.00 868.56\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Conversion achieved = 80.0%\n" + ] + } + ], + "source": [ + "m.fs.R101.report()\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimizing Hydrogen Production\n", + "\n", + "Now that the flowsheet has been squared and solved, we can run a small optimization problem to determine optimal conditions for producing hydrogen. Suppose we wish to find ideal conditions for the competing reactions. As mentioned earlier, the two reactions have competing equilibria - steam methane reformation occurs more readily at higher temperatures (500-700 C) while water gas shift occurs more readily at lower temperatures (300-400 C). We will allow for variable reactor temperature and pressure by freeing our heater and compressor specifications, and minimize cost to achieve 90% methane conversion. Since we assume an isentopic compressor, allowing compression will heat our feed stream and reduce or eliminate the required heater duty." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us declare our objective function for this problem. " + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.objective = Objective(expr=m.fs.operating_cost)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem until now, as well as set bounds for the design variables (reactor outlet temperature is set by state variable bounds in property package):" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:30:21 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" - ] - } - ], - "source": [ - "# Initialize and solve each unit operation\n", - "m.fs.CH4.initialize()\n", - "propagate_state(arc=m.fs.s01)\n", - "\n", - "m.fs.H2O.initialize()\n", - "propagate_state(arc=m.fs.s02)\n", - "\n", - "m.fs.M101.initialize()\n", - "propagate_state(arc=m.fs.s03)\n", - "\n", - "m.fs.C101.initialize()\n", - "propagate_state(arc=m.fs.s04)\n", - "\n", - "m.fs.H101.initialize()\n", - "propagate_state(arc=m.fs.s05)\n", - "\n", - "m.fs.R101.initialize()\n", - "propagate_state(arc=m.fs.s06)\n", - "\n", - "m.fs.PROD.initialize()\n", - "\n", - "# set solver\n", - "solver = get_solver()" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "scrolled": true - }, - "outputs": [ + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.conversion.fix(0.90)\n", + "\n", + "m.fs.C101.outlet.pressure.unfix()\n", + "m.fs.C101.outlet.pressure[0].setlb(\n", + " pyunits.convert(2 * pyunits.bar, to_units=pyunits.Pa)\n", + ") # pressurize to at least 2 bar\n", + "m.fs.C101.outlet.pressure[0].setub(\n", + " pyunits.convert(10 * pyunits.bar, to_units=pyunits.Pa)\n", + ") # at most, pressurize to 10 bar\n", + "\n", + "m.fs.H101.outlet.temperature.unfix()\n", + "m.fs.H101.heat_duty[0].setlb(\n", + " 0 * pyunits.J / pyunits.s\n", + ") # outlet temperature is equal to or greater than inlet temperature\n", + "m.fs.H101.outlet.temperature[0].setub(1000 * pyunits.K) # at most, heat to 1000 K" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "We have now defined the optimization problem and we are now ready to solve this problem. \n", + "\n", + "\n" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 562\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 477\n", - "\n", - "Total number of variables............................: 204\n", - " variables with only lower bounds: 13\n", - " variables with lower and upper bounds: 174\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 204\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.49e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 1.35e+04 2.00e-01 -1.0 3.59e+00 - 9.90e-01 9.91e-01h 1\n", - " 2 0.0000000e+00 3.59e-04 9.99e+00 -1.0 3.56e+00 - 9.90e-01 1.00e+00h 1\n", - " 3 0.0000000e+00 2.49e-08 8.98e+01 -1.0 2.91e-04 - 9.90e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 3\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 2.8421709430404007e-14 2.4912878870964050e-08\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 2.8421709430404007e-14 2.4912878870964050e-08\n", - "\n", - "\n", - "Number of objective function evaluations = 4\n", - "Number of objective gradient evaluations = 4\n", - "Number of equality constraint evaluations = 4\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 4\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 3\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "# Solve the model\n", - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Analyze the Results of the Square Problem\n", - "\n", - "\n", - "What is the total operating cost? " - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 29, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 569\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 493\n", + "\n", + "Total number of variables............................: 206\n", + " variables with only lower bounds: 14\n", + " variables with lower and upper bounds: 176\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 204\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 4.5933014e+07 1.49e+06 3.46e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 4.5420427e+07 1.49e+06 1.33e+03 -1.0 1.08e+07 - 4.58e-01 5.96e-03f 1\n", + " 2 4.2830345e+07 8.68e+05 6.47e+06 -1.0 5.32e+06 - 8.03e-01 4.18e-01f 1\n", + " 3 4.3111576e+07 1.26e+05 1.06e+07 -1.0 2.54e+06 - 9.54e-01 8.85e-01h 1\n", + " 4 4.3307552e+07 2.24e+03 3.12e+05 -1.0 3.51e+05 - 9.89e-01 9.86e-01h 1\n", + " 5 4.3309118e+07 2.20e+01 3.08e+03 -1.0 2.69e+03 - 9.90e-01 9.90e-01h 1\n", + " 6 4.3309131e+07 5.77e-06 3.84e+01 -1.0 2.31e+01 - 9.92e-01 1.00e+00h 1\n", + " 7 4.3309131e+07 7.77e-09 4.84e-07 -2.5 1.97e-02 - 1.00e+00 1.00e+00f 1\n", + " 8 4.3309131e+07 1.63e-08 1.71e-06 -3.8 5.56e-04 - 1.00e+00 1.00e+00f 1\n", + " 9 4.3309131e+07 1.72e-08 1.31e-06 -5.7 3.08e-05 - 1.00e+00 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 4.3309131e+07 2.20e-08 8.55e-07 -7.0 3.59e-07 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 10\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 4.3309130854568794e+07 4.3309130854568794e+07\n", + "Dual infeasibility......: 8.5488594337511447e-07 8.5488594337511447e-07\n", + "Constraint violation....: 1.4551915228366852e-11 2.2002495825290680e-08\n", + "Complementarity.........: 9.0909090913936433e-08 9.0909090913936433e-08\n", + "Overall NLP error.......: 9.0909090913936433e-08 8.5488594337511447e-07\n", + "\n", + "\n", + "Number of objective function evaluations = 11\n", + "Number of objective gradient evaluations = 11\n", + "Number of equality constraint evaluations = 11\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 11\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.008\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], + "source": [ + "results = solver.solve(m, tee=True)" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $45.933 million per year\n" - ] - } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For this operating cost, what conversion did we achieve of methane to hydrogen?" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $43.309 million per year\n", + "\n", + "Compressor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.C101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Isentropic Efficiency : 0.90000 : dimensionless : True : (None, None)\n", + " Mechanical Work : 7.5471e+05 : watt : False : (None, None)\n", + " Pressure Change : 1.0000e+05 : pascal : False : (None, None)\n", + " Pressure Ratio : 2.0000 : dimensionless : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 353.80 423.34\n", + " Pressure pascal 1.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Heater results\n", + "\n", + "====================================================================================\n", + "Unit : fs.H101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 5.8781e-09 : watt : False : (0.0, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 423.34 423.34\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Equilibrium reactor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 3.2486e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 444.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.016892\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.29075\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.54032\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.067801\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.084239\n", + " Temperature kelvin 423.34 910.04\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "\n", + "print()\n", + "print(\"Compressor results\")\n", + "\n", + "m.fs.C101.report()\n", + "\n", + "print()\n", + "print(\"Heater results\")\n", + "\n", + "m.fs.H101.report()\n", + "\n", + "print()\n", + "print(\"Equilibrium reactor results\")\n", + "\n", + "m.fs.R101.report()" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "====================================================================================\n", - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : 2.7605e+07 : watt : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Total Molar Flowrate mole / second 309.01 429.02\n", - " Total Mole Fraction CH4 dimensionless 0.24272 0.034965\n", - " Total Mole Fraction H2O dimensionless 0.75725 0.31487\n", - " Total Mole Fraction H2 dimensionless 9.9996e-06 0.51029\n", - " Total Mole Fraction CO dimensionless 9.9996e-06 0.049157\n", - " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.090717\n", - " Temperature kelvin 500.00 868.56\n", - " Pressure pascal 2.0000e+05 2.0000e+05\n", - "====================================================================================\n", - "\n", - "Conversion achieved = 80.0%\n" - ] - } - ], - "source": [ - "m.fs.R101.report()\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Optimizing Hydrogen Production\n", - "\n", - "Now that the flowsheet has been squared and solved, we can run a small optimization problem to determine optimal conditions for producing hydrogen. Suppose we wish to find ideal conditions for the competing reactions. As mentioned earlier, the two reactions have competing equilibria - steam methane reformation occurs more readily at higher temperatures (500-700 C) while water gas shift occurs more readily at lower temperatures (300-400 C). We will allow for variable reactor temperature and pressure by freeing our heater and compressor specifications, and minimize cost to achieve 90% methane conversion. Since we assume an isentopic compressor, allowing compression will heat our feed stream and reduce or eliminate the required heater duty." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us declare our objective function for this problem. " - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.objective = Objective(expr=m.fs.operating_cost)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem until now, as well as set bounds for the design variables (reactor outlet temperature is set by state variable bounds in property package):" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.R101.conversion.fix(0.90)\n", - "\n", - "m.fs.C101.outlet.pressure.unfix()\n", - "m.fs.C101.outlet.pressure[0].setlb(\n", - " pyunits.convert(2 * pyunits.bar, to_units=pyunits.Pa)\n", - ") # pressurize to at least 2 bar\n", - "m.fs.C101.outlet.pressure[0].setub(\n", - " pyunits.convert(10 * pyunits.bar, to_units=pyunits.Pa)\n", - ") # at most, pressurize to 10 bar\n", - "\n", - "m.fs.H101.outlet.temperature.unfix()\n", - "m.fs.H101.heat_duty[0].setlb(\n", - " 0 * pyunits.J / pyunits.s\n", - ") # outlet temperature is equal to or greater than inlet temperature\n", - "m.fs.H101.outlet.temperature[0].setub(1000 * pyunits.K) # at most, heat to 1000 K" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "We have now defined the optimization problem and we are now ready to solve this problem. \n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "scrolled": true - }, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display optimal values for the decision variables and design variables:" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 569\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 493\n", - "\n", - "Total number of variables............................: 206\n", - " variables with only lower bounds: 14\n", - " variables with lower and upper bounds: 176\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 204\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 4.5933014e+07 1.49e+06 3.46e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 4.5420427e+07 1.49e+06 1.33e+03 -1.0 1.08e+07 - 4.58e-01 5.96e-03f 1\n", - " 2 4.2830345e+07 8.68e+05 6.47e+06 -1.0 5.32e+06 - 8.03e-01 4.18e-01f 1\n", - " 3 4.3111576e+07 1.26e+05 1.06e+07 -1.0 2.54e+06 - 9.54e-01 8.85e-01h 1\n", - " 4 4.3307552e+07 2.24e+03 3.12e+05 -1.0 3.51e+05 - 9.89e-01 9.86e-01h 1\n", - " 5 4.3309118e+07 2.20e+01 3.08e+03 -1.0 2.69e+03 - 9.90e-01 9.90e-01h 1\n", - " 6 4.3309131e+07 5.79e-06 3.84e+01 -1.0 2.31e+01 - 9.92e-01 1.00e+00h 1\n", - " 7 4.3309131e+07 7.77e-09 1.30e-06 -2.5 1.97e-02 - 1.00e+00 1.00e+00f 1\n", - " 8 4.3309131e+07 2.20e-08 1.95e-06 -3.8 5.56e-04 - 1.00e+00 1.00e+00f 1\n", - " 9 4.3309131e+07 1.79e-08 1.27e-06 -5.7 3.08e-05 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 4.3309131e+07 5.88e-09 1.09e-06 -7.0 3.56e-07 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 10\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 4.3309130854568668e+07 4.3309130854568668e+07\n", - "Dual infeasibility......: 1.0873799957492332e-06 1.0873799957492332e-06\n", - "Constraint violation....: 1.4551915228366852e-11 5.8780968648563987e-09\n", - "Complementarity.........: 9.0909090913936446e-08 9.0909090913936446e-08\n", - "Overall NLP error.......: 9.0909090913936446e-08 1.0873799957492332e-06\n", - "\n", - "\n", - "Number of objective function evaluations = 11\n", - "Number of objective gradient evaluations = 11\n", - "Number of equality constraint evaluations = 11\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 11\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 10\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", - "Total CPU secs in NLP function evaluations = 0.007\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimal Values\n", + "\n", + "C101 outlet pressure = 0.200 MPa\n", + "\n", + "C101 outlet temperature = 423.345 K\n", + "\n", + "H101 outlet temperature = 423.345 K\n", + "\n", + "R101 outlet temperature = 910.044 K\n", + "\n", + "Hydrogen produced = 33.648 MM lb/year\n", + "\n", + "Conversion achieved = 90.0%\n" + ] + } + ], + "source": [ + "print(\"Optimal Values\")\n", + "print()\n", + "\n", + "print(f\"C101 outlet pressure = {value(m.fs.C101.outlet.pressure[0])/1E6:0.3f} MPa\")\n", + "print()\n", + "\n", + "print(f\"C101 outlet temperature = {value(m.fs.C101.outlet.temperature[0]):0.3f} K\")\n", + "print()\n", + "\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "\n", + "print()\n", + "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.3f} K\")\n", + "\n", + "print()\n", + "print(f\"Hydrogen produced = {value(m.fs.hyd_prod):0.3f} MM lb/year\")\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $43.309 million per year\n", - "\n", - "Compressor results\n", - "\n", - "====================================================================================\n", - "Unit : fs.C101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Isentropic Efficiency : 0.90000 : dimensionless : True : (None, None)\n", - " Mechanical Work : 7.5471e+05 : watt : False : (None, None)\n", - " Pressure Change : 1.0000e+05 : pascal : False : (None, None)\n", - " Pressure Ratio : 2.0000 : dimensionless : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Total Molar Flowrate mole / second 309.01 309.01\n", - " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", - " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", - " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", - " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", - " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", - " Temperature kelvin 353.80 423.34\n", - " Pressure pascal 1.0000e+05 2.0000e+05\n", - "====================================================================================\n", - "\n", - "Heater results\n", - "\n", - "====================================================================================\n", - "Unit : fs.H101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : 5.8781e-09 : watt : False : (0.0, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Total Molar Flowrate mole / second 309.01 309.01\n", - " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", - " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", - " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", - " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", - " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", - " Temperature kelvin 423.34 423.34\n", - " Pressure pascal 2.0000e+05 2.0000e+05\n", - "====================================================================================\n", - "\n", - "Equilibrium reactor results\n", - "\n", - "====================================================================================\n", - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : 3.2486e+07 : watt : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Total Molar Flowrate mole / second 309.01 444.02\n", - " Total Mole Fraction CH4 dimensionless 0.24272 0.016892\n", - " Total Mole Fraction H2O dimensionless 0.75725 0.29075\n", - " Total Mole Fraction H2 dimensionless 9.9996e-06 0.54032\n", - " Total Mole Fraction CO dimensionless 9.9996e-06 0.067801\n", - " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.084239\n", - " Temperature kelvin 423.34 910.04\n", - " Pressure pascal 2.0000e+05 2.0000e+05\n", - "====================================================================================\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", - "\n", - "print()\n", - "print(\"Compressor results\")\n", - "\n", - "m.fs.C101.report()\n", - "\n", - "print()\n", - "print(\"Heater results\")\n", - "\n", - "m.fs.H101.report()\n", - "\n", - "print()\n", - "print(\"Equilibrium reactor results\")\n", - "\n", - "m.fs.R101.report()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Display optimal values for the decision variables and design variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimal Values\n", - "\n", - "C101 outlet pressure = 0.200 MPa\n", - "\n", - "C101 outlet temperature = 423.345 K\n", - "\n", - "H101 outlet temperature = 423.345 K\n", - "\n", - "R101 outlet temperature = 910.044 K\n", - "\n", - "Hydrogen produced = 33.648 MM lb/year\n", - "\n", - "Conversion achieved = 90.0%\n" - ] + ], + "metadata": { + "celltoolbar": "Tags", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" } - ], - "source": [ - "print(\"Optimal Values\")\n", - "print()\n", - "\n", - "print(f\"C101 outlet pressure = {value(m.fs.C101.outlet.pressure[0])/1E6:0.3f} MPa\")\n", - "print()\n", - "\n", - "print(f\"C101 outlet temperature = {value(m.fs.C101.outlet.temperature[0]):0.3f} K\")\n", - "print()\n", - "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", - "\n", - "print()\n", - "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.3f} K\")\n", - "\n", - "print()\n", - "print(f\"Hydrogen produced = {value(m.fs.hyd_prod):0.3f} MM lb/year\")\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "celltoolbar": "Tags", - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 3 -} + "nbformat": 4, + "nbformat_minor": 3 +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_test.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_test.ipynb index 9adb969e..a845483e 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_test.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_test.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "tags": [ "header", @@ -54,8 +54,8 @@ "\n", "Steam methane reforming (SMR) is one of the most common pathways for hydrogen production, taking advantage of chemical equilibria in natural gas systems. The process is typically done in two steps: methane reformation at a high temperature to partially oxidize methane, and water gas shift at a low temperature to complete the oxidation reaction:\n", "\n", - "**CH4 + H2O → CO + 3H2** \n", - "**CO + H2O → CO2 + H2**\n", + "**CH4 + H2O \u2192 CO + 3H2** \n", + "**CO + H2O \u2192 CO2 + H2**\n", "\n", "This reaction is often carried out in two separate reactors to allow for different reaction temperatures and pressures; in this example, we will minimize operating cost for a single reactor.\n", "\n", @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -166,7 +166,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -185,7 +185,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -202,7 +202,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "scrolled": true }, @@ -226,7 +226,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "scrolled": true }, @@ -248,10 +248,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, + "execution_count": 7, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = EquilibriumReactor(\n", @@ -276,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -297,7 +295,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -319,7 +317,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -342,7 +340,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -373,18 +371,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "20\n" + ] + } + ], "source": [ "print(degrees_of_freedom(m))" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": { "tags": [ "testing" @@ -405,7 +411,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -439,7 +445,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -457,7 +463,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -487,16 +493,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n" + ] + } + ], "source": [ "print(degrees_of_freedom(m))" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": { "tags": [ "testing" @@ -517,9 +531,58 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4.properties: Starting initialization\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4: Initialization Complete.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O.properties: Starting initialization\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O: Initialization Complete.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.M101.methane_feed_state: Starting initialization\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.M101.methane_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.M101.steam_feed_state: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.steam_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.properties_isentropic: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.properties_isentropic: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.properties_isentropic: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.C101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.reactions: Initialization Complete.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" + ] + } + ], "source": [ "# Initialize and solve each unit operation\n", "m.fs.CH4.initialize()\n", @@ -548,11 +611,84 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 562\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 477\n", + "\n", + "Total number of variables............................: 204\n", + " variables with only lower bounds: 13\n", + " variables with lower and upper bounds: 174\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 204\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.49e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 1.35e+04 2.00e-01 -1.0 3.59e+00 - 9.90e-01 9.91e-01h 1\n", + " 2 0.0000000e+00 3.59e-04 9.99e+00 -1.0 3.56e+00 - 9.90e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 2.12e-08 8.98e+01 -1.0 2.91e-04 - 9.90e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 3\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.8421709430404007e-14 2.1187588572502136e-08\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.8421709430404007e-14 2.1187588572502136e-08\n", + "\n", + "\n", + "Number of objective function evaluations = 4\n", + "Number of objective gradient evaluations = 4\n", + "Number of equality constraint evaluations = 4\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 4\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 3\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.003\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], "source": [ "# Solve the model\n", "results = solver.solve(m, tee=True)" @@ -560,7 +696,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": { "tags": [ "testing" @@ -586,16 +722,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $45.933 million per year\n" + ] + } + ], "source": [ "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": { "tags": [ "testing" @@ -617,9 +761,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 2.7605e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 429.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.034965\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.31487\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.51029\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.049157\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.090717\n", + " Temperature kelvin 500.00 868.56\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Conversion achieved = 80.0%\n" + ] + } + ], "source": [ "m.fs.R101.report()\n", "\n", @@ -629,7 +805,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": { "tags": [ "testing" @@ -660,7 +836,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ @@ -676,7 +852,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -699,7 +875,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "metadata": { "tags": [ "testing" @@ -722,18 +898,99 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 569\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 493\n", + "\n", + "Total number of variables............................: 206\n", + " variables with only lower bounds: 14\n", + " variables with lower and upper bounds: 176\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 204\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 4.5933014e+07 1.49e+06 3.46e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 4.5420427e+07 1.49e+06 1.33e+03 -1.0 1.08e+07 - 4.58e-01 5.96e-03f 1\n", + " 2 4.2830345e+07 8.68e+05 6.47e+06 -1.0 5.32e+06 - 8.03e-01 4.18e-01f 1\n", + " 3 4.3111576e+07 1.26e+05 1.06e+07 -1.0 2.54e+06 - 9.54e-01 8.85e-01h 1\n", + " 4 4.3307552e+07 2.24e+03 3.12e+05 -1.0 3.51e+05 - 9.89e-01 9.86e-01h 1\n", + " 5 4.3309118e+07 2.20e+01 3.08e+03 -1.0 2.69e+03 - 9.90e-01 9.90e-01h 1\n", + " 6 4.3309131e+07 5.77e-06 3.84e+01 -1.0 2.31e+01 - 9.92e-01 1.00e+00h 1\n", + " 7 4.3309131e+07 7.77e-09 4.84e-07 -2.5 1.97e-02 - 1.00e+00 1.00e+00f 1\n", + " 8 4.3309131e+07 1.63e-08 1.71e-06 -3.8 5.56e-04 - 1.00e+00 1.00e+00f 1\n", + " 9 4.3309131e+07 1.72e-08 1.31e-06 -5.7 3.08e-05 - 1.00e+00 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 4.3309131e+07 2.20e-08 8.55e-07 -7.0 3.59e-07 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 10\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 4.3309130854568794e+07 4.3309130854568794e+07\n", + "Dual infeasibility......: 8.5488594337511447e-07 8.5488594337511447e-07\n", + "Constraint violation....: 1.4551915228366852e-11 2.2002495825290680e-08\n", + "Complementarity.........: 9.0909090913936433e-08 9.0909090913936433e-08\n", + "Overall NLP error.......: 9.0909090913936433e-08 8.5488594337511447e-07\n", + "\n", + "\n", + "Number of objective function evaluations = 11\n", + "Number of objective gradient evaluations = 11\n", + "Number of equality constraint evaluations = 11\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 11\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.008\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], "source": [ "results = solver.solve(m, tee=True)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "metadata": { "tags": [ "testing" @@ -749,9 +1006,95 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $43.309 million per year\n", + "\n", + "Compressor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.C101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Isentropic Efficiency : 0.90000 : dimensionless : True : (None, None)\n", + " Mechanical Work : 7.5471e+05 : watt : False : (None, None)\n", + " Pressure Change : 1.0000e+05 : pascal : False : (None, None)\n", + " Pressure Ratio : 2.0000 : dimensionless : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 353.80 423.34\n", + " Pressure pascal 1.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Heater results\n", + "\n", + "====================================================================================\n", + "Unit : fs.H101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 5.8781e-09 : watt : False : (0.0, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 423.34 423.34\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Equilibrium reactor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 3.2486e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 444.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.016892\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.29075\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.54032\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.067801\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.084239\n", + " Temperature kelvin 423.34 910.04\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n" + ] + } + ], "source": [ "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", "\n", @@ -773,7 +1116,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "metadata": { "tags": [ "testing" @@ -793,9 +1136,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimal Values\n", + "\n", + "C101 outlet pressure = 0.200 MPa\n", + "\n", + "C101 outlet temperature = 423.345 K\n", + "\n", + "H101 outlet temperature = 423.345 K\n", + "\n", + "R101 outlet temperature = 910.044 K\n", + "\n", + "Hydrogen produced = 33.648 MM lb/year\n", + "\n", + "Conversion achieved = 90.0%\n" + ] + } + ], "source": [ "print(\"Optimal Values\")\n", "print()\n", @@ -820,7 +1183,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": { "tags": [ "testing" @@ -861,9 +1224,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_usr.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_usr.ipynb index 20b6e5a8..6185b931 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_usr.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/equilibrium_reactor_usr.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "tags": [ "header", @@ -54,8 +54,8 @@ "\n", "Steam methane reforming (SMR) is one of the most common pathways for hydrogen production, taking advantage of chemical equilibria in natural gas systems. The process is typically done in two steps: methane reformation at a high temperature to partially oxidize methane, and water gas shift at a low temperature to complete the oxidation reaction:\n", "\n", - "**CH4 + H2O → CO + 3H2** \n", - "**CO + H2O → CO2 + H2**\n", + "**CH4 + H2O \u2192 CO + 3H2** \n", + "**CO + H2O \u2192 CO2 + H2**\n", "\n", "This reaction is often carried out in two separate reactors to allow for different reaction temperatures and pressures; in this example, we will minimize operating cost for a single reactor.\n", "\n", @@ -97,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -166,7 +166,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -185,7 +185,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -202,7 +202,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "scrolled": true }, @@ -226,7 +226,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "scrolled": true }, @@ -248,10 +248,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, + "execution_count": 7, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = EquilibriumReactor(\n", @@ -276,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -297,7 +295,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -319,7 +317,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -342,7 +340,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -373,11 +371,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "20\n" + ] + } + ], "source": [ "print(degrees_of_freedom(m))" ] @@ -391,7 +397,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -425,7 +431,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -443,7 +449,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -473,9 +479,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n" + ] + } + ], "source": [ "print(degrees_of_freedom(m))" ] @@ -489,9 +503,58 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4.properties: Starting initialization\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.CH4: Initialization Complete.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O.properties: Starting initialization\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.H2O: Initialization Complete.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.M101.methane_feed_state: Starting initialization\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.M101.methane_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:03 [INFO] idaes.init.fs.M101.steam_feed_state: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.steam_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.properties_isentropic: Starting initialization\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.properties_isentropic: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:04 [INFO] idaes.init.fs.C101.properties_isentropic: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.C101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume.reactions: Initialization Complete.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:08:05 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" + ] + } + ], "source": [ "# Initialize and solve each unit operation\n", "m.fs.CH4.initialize()\n", @@ -520,11 +583,84 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 562\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 477\n", + "\n", + "Total number of variables............................: 204\n", + " variables with only lower bounds: 13\n", + " variables with lower and upper bounds: 174\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 204\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.49e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 1.35e+04 2.00e-01 -1.0 3.59e+00 - 9.90e-01 9.91e-01h 1\n", + " 2 0.0000000e+00 3.59e-04 9.99e+00 -1.0 3.56e+00 - 9.90e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 2.12e-08 8.98e+01 -1.0 2.91e-04 - 9.90e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 3\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 2.8421709430404007e-14 2.1187588572502136e-08\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 2.8421709430404007e-14 2.1187588572502136e-08\n", + "\n", + "\n", + "Number of objective function evaluations = 4\n", + "Number of objective gradient evaluations = 4\n", + "Number of equality constraint evaluations = 4\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 4\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 3\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.003\n", + "Total CPU secs in NLP function evaluations = 0.001\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], "source": [ "# Solve the model\n", "results = solver.solve(m, tee=True)" @@ -542,9 +678,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $45.933 million per year\n" + ] + } + ], "source": [ "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" ] @@ -558,9 +702,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 2.7605e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 429.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.034965\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.31487\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.51029\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.049157\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.090717\n", + " Temperature kelvin 500.00 868.56\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Conversion achieved = 80.0%\n" + ] + } + ], "source": [ "m.fs.R101.report()\n", "\n", @@ -586,7 +762,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ @@ -602,7 +778,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -635,20 +811,187 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 569\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 493\n", + "\n", + "Total number of variables............................: 206\n", + " variables with only lower bounds: 14\n", + " variables with lower and upper bounds: 176\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 204\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 4.5933014e+07 1.49e+06 3.46e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 4.5420427e+07 1.49e+06 1.33e+03 -1.0 1.08e+07 - 4.58e-01 5.96e-03f 1\n", + " 2 4.2830345e+07 8.68e+05 6.47e+06 -1.0 5.32e+06 - 8.03e-01 4.18e-01f 1\n", + " 3 4.3111576e+07 1.26e+05 1.06e+07 -1.0 2.54e+06 - 9.54e-01 8.85e-01h 1\n", + " 4 4.3307552e+07 2.24e+03 3.12e+05 -1.0 3.51e+05 - 9.89e-01 9.86e-01h 1\n", + " 5 4.3309118e+07 2.20e+01 3.08e+03 -1.0 2.69e+03 - 9.90e-01 9.90e-01h 1\n", + " 6 4.3309131e+07 5.77e-06 3.84e+01 -1.0 2.31e+01 - 9.92e-01 1.00e+00h 1\n", + " 7 4.3309131e+07 7.77e-09 4.84e-07 -2.5 1.97e-02 - 1.00e+00 1.00e+00f 1\n", + " 8 4.3309131e+07 1.63e-08 1.71e-06 -3.8 5.56e-04 - 1.00e+00 1.00e+00f 1\n", + " 9 4.3309131e+07 1.72e-08 1.31e-06 -5.7 3.08e-05 - 1.00e+00 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 4.3309131e+07 2.20e-08 8.55e-07 -7.0 3.59e-07 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 10\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 4.3309130854568794e+07 4.3309130854568794e+07\n", + "Dual infeasibility......: 8.5488594337511447e-07 8.5488594337511447e-07\n", + "Constraint violation....: 1.4551915228366852e-11 2.2002495825290680e-08\n", + "Complementarity.........: 9.0909090913936433e-08 9.0909090913936433e-08\n", + "Overall NLP error.......: 9.0909090913936433e-08 8.5488594337511447e-07\n", + "\n", + "\n", + "Number of objective function evaluations = 11\n", + "Number of objective gradient evaluations = 11\n", + "Number of equality constraint evaluations = 11\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 11\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.008\n", + "Total CPU secs in NLP function evaluations = 0.003\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], "source": [ "results = solver.solve(m, tee=True)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $43.309 million per year\n", + "\n", + "Compressor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.C101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Isentropic Efficiency : 0.90000 : dimensionless : True : (None, None)\n", + " Mechanical Work : 7.5471e+05 : watt : False : (None, None)\n", + " Pressure Change : 1.0000e+05 : pascal : False : (None, None)\n", + " Pressure Ratio : 2.0000 : dimensionless : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 353.80 423.34\n", + " Pressure pascal 1.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Heater results\n", + "\n", + "====================================================================================\n", + "Unit : fs.H101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 5.8781e-09 : watt : False : (0.0, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 423.34 423.34\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Equilibrium reactor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 3.2486e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 444.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.016892\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.29075\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.54032\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.067801\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.084239\n", + " Temperature kelvin 423.34 910.04\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n" + ] + } + ], "source": [ "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", "\n", @@ -677,9 +1020,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimal Values\n", + "\n", + "C101 outlet pressure = 0.200 MPa\n", + "\n", + "C101 outlet temperature = 423.345 K\n", + "\n", + "H101 outlet temperature = 423.345 K\n", + "\n", + "R101 outlet temperature = 910.044 K\n", + "\n", + "Hydrogen produced = 33.648 MM lb/year\n", + "\n", + "Conversion achieved = 90.0%\n" + ] + } + ], "source": [ "print(\"Optimal Values\")\n", "print()\n", @@ -727,9 +1090,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_doc.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_doc.ipynb index 11dbd1da..069a9a84 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_doc.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_doc.ipynb @@ -1,1298 +1,1049 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "tags": [ - "header", - "hide-cell" - ] - }, - "outputs": [], - "source": [ - "###############################################################################\n", - "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", - "# Framework (IDAES IP) was produced under the DOE Institute for the\n", - "# Design of Advanced Energy Systems (IDAES).\n", - "#\n", - "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", - "# University of California, through Lawrence Berkeley National Laboratory,\n", - "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", - "# University, West Virginia University Research Corporation, et al.\n", - "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", - "# for full copyright and license information.\n", - "###############################################################################" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Flowsheet Gibbs Reactor Simulation and Optimization of Steam Methane Reforming\n", - "Author: Brandon Paul \n", - "Maintainer: Brandon Paul \n", - "Updated: 2023-06-01 \n", - "\n", - "\n", - "## Learning Outcomes\n", - "\n", - "\n", - "- Call and implement the IDAES GibbsReactor unit model\n", - "- Construct a steady-state flowsheet using the IDAES unit model library\n", - "- Connecting unit models in a flowsheet using Arcs\n", - "- Fomulate and solve an optimization problem\n", - " - Defining an objective function\n", - " - Setting variable bounds\n", - " - Adding additional constraints \n", - "\n", - "\n", - "## Problem Statement\n", - "\n", - "Following the previous example of [Steam Methane Reformation in an Equilibrium Reactor](http://localhost:8888/notebooks/GitHub/examples-pse/src/Examples/UnitModels/Reactors/equilibrium_reactor_testing_doc.md), this example solves the flowsheet using a Gibbs Reactor instead. The steam methane reformation example is adapted from S.Z. Abbas, V. Dupont, T. Mahmud, Kinetics study and modelling of steam methane reforming process over a NiO/Al2O3 catalyst in an adiabatic packed bed reactor. Int. J. Hydrogen Energy, 42 (2017), pp. 2889-2903. Typically, the process follows the chemical equations below:\n", - "\n", - "**CH4 + H2O → CO + 3H2** \n", - "**CO + H2O → CO2 + H2**\n", - "\n", - "However, the GibbsReactor unit model solves the equilibrium by minimizing Gibbs free energy. Conveniently, this eliminates the need for a reaction package although a thermophysical package is still required.\n", - "\n", - "The flowsheet that we will be using for this module is shown below with the stream conditions. As in the prior example, we will be processing natural gas and steam feeds of fixed composition to produce hydrogen. The process consists of a mixer M101 for the two inlet streams, a compressor to compress the feed to the reaction pressure, a heater H101 to heat the feed to the reaction temperature, and a GibbsReactor unit R101. We will use thermophysical properties following the Peng-Robinsion cubic equation of state for this flowsheet.\n", - "\n", - "The state variables chosen for the property package are **total molar flows of each stream, temperature of each stream and pressure of each stream, and mole fractions of each component in each stream**. The components considered are: **CH4, H2O, CO, CO2, and H2** and the process occurs in vapor phase only. Therefore, every stream has 1 flow variable, 5 mole fraction variables, 1 temperature and 1 pressure variable. \n", - "\n", - "![](msr_flowsheet.png)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Pyomo and IDAES Components\n", - "\n", - "\n", - "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages, as well as some utility tools to build the flowsheet. For further details on these components, please refer to the [Pyomo documentation]( https://pyomo.readthedocs.io/en/stable/).\n", - "\n", - "From IDAES, we will be needing the `FlowsheetBlock` and the following unit models:\n", - "- Feed\n", - "- Mixer\n", - "- Compressor\n", - "- Heater\n", - "- GibbsReactor\n", - "- Product" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from pyomo.environ import (\n", - " Constraint,\n", - " Var,\n", - " ConcreteModel,\n", - " Expression,\n", - " Objective,\n", - " TransformationFactory,\n", - " value,\n", - " units as pyunits,\n", - ")\n", - "from pyomo.network import Arc\n", - "\n", - "from idaes.core import FlowsheetBlock\n", - "from idaes.models.properties.modular_properties import GenericParameterBlock\n", - "from idaes.models.unit_models import (\n", - " Feed,\n", - " Mixer,\n", - " Compressor,\n", - " Heater,\n", - " GibbsReactor,\n", - " Product,\n", - ")\n", - "\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.util.initialization import propagate_state" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Thermophysical Package\n", - "\n", - "As mentioned earlier, the `GibbsReactor` does not require a reaction package.\n", - "\n", - "Let us import the following module from the IDAES library:\n", - "- natural_gas_PR as get_prop (method to get configuration dictionary)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Constructing the Flowsheet\n", - "\n", - "Let us create a `ConcreteModel` and add the flowsheet block. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "m = ConcreteModel()\n", - "m.fs = FlowsheetBlock(dynamic=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now need to add the property packages to the flowsheet. Unlike the previous example, we do not need to add a reaction package for the reactor model to calculate results. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) to build a state block for the parameter dictionary." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "thermo_props_config_dict = get_prop(components=[\"CH4\", \"H2O\", \"H2\", \"CO\", \"CO2\"])\n", - "m.fs.thermo_params = GenericParameterBlock(**thermo_props_config_dict)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Unit Models\n", - "\n", - "Let us start adding the unit models we have imported to the flowsheet:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.fs.CH4 = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.H2O = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", - "m.fs.M101 = Mixer(\n", - " property_package=m.fs.thermo_params, inlet_list=[\"methane_feed\", \"steam_feed\"]\n", - ")\n", - "m.fs.H101 = Heater(\n", - " property_package=m.fs.thermo_params,\n", - " has_pressure_change=False,\n", - " has_phase_equilibrium=False,\n", - ")\n", - "m.fs.C101 = Compressor(property_package=m.fs.thermo_params)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "m.fs.R101 = GibbsReactor(\n", - " property_package=m.fs.thermo_params,\n", - " has_heat_transfer=True,\n", - " has_pressure_change=False,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Connecting Unit Models Using Arcs\n", - "\n", - "We have now added all the unit models we need to the flowsheet. Let us connect the unit models by defining and building each `Arc`:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.s01 = Arc(source=m.fs.CH4.outlet, destination=m.fs.M101.methane_feed)\n", - "m.fs.s02 = Arc(source=m.fs.H2O.outlet, destination=m.fs.M101.steam_feed)\n", - "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.C101.inlet)\n", - "m.fs.s04 = Arc(source=m.fs.C101.outlet, destination=m.fs.H101.inlet)\n", - "m.fs.s05 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", - "m.fs.s06 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we can use Pyomo's `TransformationFactory` to write the equality constraints on each Arc:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "TransformationFactory(\"network.expand_arcs\").apply_to(m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Expressions to Compute Operating Costs\n", - "\n", - "For this flowsheet, we are interested in computing hydrogen production in millions of pounds per year, as well as the total costs due to pressurizing, cooling, and heating utilities:" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us add `Expressions` to convert the product flow from mol/s to MM lb/year of hydrogen, and to calculate the cooling, heating and compression operating costs. The total operating cost will be the sum of the costs, expressed in \\\\$/year assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.hyd_prod = Expression(\n", - " expr=pyunits.convert(\n", - " m.fs.PROD.inlet.flow_mol[0]\n", - " * m.fs.PROD.inlet.mole_frac_comp[0, \"H2\"]\n", - " * m.fs.thermo_params.H2.mw, # MW defined in properties as kg/mol\n", - " to_units=pyunits.Mlb / pyunits.yr,\n", - " )\n", - ") # converting kg/s to MM lb/year" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.cooling_cost = Expression(\n", - " expr=0.212e-7 * (m.fs.R101.heat_duty[0])\n", - ") # the reaction is endothermic, so R101 duty is positive\n", - "m.fs.heating_cost = Expression(\n", - " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", - ") # the stream must be heated to T_rxn, so H101 duty is positive\n", - "m.fs.compression_cost = Expression(\n", - " expr=0.12e-5 * m.fs.C101.work_isentropic[0]\n", - ") # the stream must be pressurized, so the C101 work is positive\n", - "m.fs.operating_cost = Expression(\n", - " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost + m.fs.compression_cost))\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Feed Conditions\n", - "\n", - "We expect each stream to have 8 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the compressor to have 2 (the pressure change and efficiency), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 1 (conversion). Therefore, we have 20 degrees of freedom to specify: temperature, pressure, flow and mole fractions of all five components on both streams; compressor pressure change and efficiency; outlet heater temperature; and reactor conversion." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "scrolled": true - }, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "20\n" - ] - } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Based on the literature source, we will initialize our simulation with the following values:" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.CH4.outlet.mole_frac_comp[0, \"CH4\"].fix(1)\n", - "m.fs.CH4.outlet.mole_frac_comp[0, \"H2O\"].fix(1e-5)\n", - "m.fs.CH4.outlet.mole_frac_comp[0, \"H2\"].fix(1e-5)\n", - "m.fs.CH4.outlet.mole_frac_comp[0, \"CO\"].fix(1e-5)\n", - "m.fs.CH4.outlet.mole_frac_comp[0, \"CO2\"].fix(1e-5)\n", - "m.fs.CH4.outlet.flow_mol.fix(75 * pyunits.mol / pyunits.s)\n", - "m.fs.CH4.outlet.temperature.fix(298.15 * pyunits.K)\n", - "m.fs.CH4.outlet.pressure.fix(1e5 * pyunits.Pa)\n", - "\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"CH4\"].fix(1e-5)\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"H2O\"].fix(1)\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"H2\"].fix(1e-5)\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"CO\"].fix(1e-5)\n", - "m.fs.H2O.outlet.mole_frac_comp[0, \"CO2\"].fix(1e-5)\n", - "m.fs.H2O.outlet.flow_mol.fix(234 * pyunits.mol / pyunits.s)\n", - "m.fs.H2O.outlet.temperature.fix(373.15 * pyunits.K)\n", - "m.fs.H2O.outlet.pressure.fix(1e5 * pyunits.Pa)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Unit Model Specifications\n", - "\n", - "For the initial problem, let us fix the compressor outlet pressure to 2 bar for now, the efficiency to 0.90 (a common assumption for compressor units), and the heater outlet temperature to 500 K. We will unfix these values later to optimize the flowsheet." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.C101.outlet.pressure.fix(pyunits.convert(2 * pyunits.bar, to_units=pyunits.Pa))\n", - "m.fs.C101.efficiency_isentropic.fix(0.90)\n", - "m.fs.H101.outlet.temperature.fix(500 * pyunits.K)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `GibbsReactor` unit model calculates the amount of product and reactant based on the free energy minimization; therefore, we will specify a desired conversion and let the solver determine the reactor duty and heat transfer. For convenience, we will define the reactor conversion as the amount of methane that is converted." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.R101.conversion = Var(\n", - " initialize=0.80, bounds=(0, 1), units=pyunits.dimensionless\n", - ") # fraction\n", - "\n", - "m.fs.R101.conv_constraint = Constraint(\n", - " expr=m.fs.R101.conversion\n", - " * m.fs.R101.inlet.flow_mol[0]\n", - " * m.fs.R101.inlet.mole_frac_comp[0, \"CH4\"]\n", - " == (\n", - " m.fs.R101.inlet.flow_mol[0] * m.fs.R101.inlet.mole_frac_comp[0, \"CH4\"]\n", - " - m.fs.R101.outlet.flow_mol[0] * m.fs.R101.outlet.mole_frac_comp[0, \"CH4\"]\n", - " )\n", - ")\n", - "\n", - "m.fs.R101.conversion.fix(0.80)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [ + "header", + "hide-cell" + ] + }, + "outputs": [], + "source": [ + "###############################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "###############################################################################" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "0" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "# Flowsheet Gibbs Reactor Simulation and Optimization of Steam Methane Reforming\n", + "Author: Brandon Paul \n", + "Maintainer: Brandon Paul \n", + "Updated: 2023-06-01 \n", + "\n", + "\n", + "## Learning Outcomes\n", + "\n", + "\n", + "- Call and implement the IDAES GibbsReactor unit model\n", + "- Construct a steady-state flowsheet using the IDAES unit model library\n", + "- Connecting unit models in a flowsheet using Arcs\n", + "- Fomulate and solve an optimization problem\n", + " - Defining an objective function\n", + " - Setting variable bounds\n", + " - Adding additional constraints \n", + "\n", + "\n", + "## Problem Statement\n", + "\n", + "Following the previous example of [Steam Methane Reformation in an Equilibrium Reactor](http://localhost:8888/notebooks/GitHub/examples-pse/src/Examples/UnitModels/Reactors/equilibrium_reactor_testing_doc.md), this example solves the flowsheet using a Gibbs Reactor instead. The steam methane reformation example is adapted from S.Z. Abbas, V. Dupont, T. Mahmud, Kinetics study and modelling of steam methane reforming process over a NiO/Al2O3 catalyst in an adiabatic packed bed reactor. Int. J. Hydrogen Energy, 42 (2017), pp. 2889-2903. Typically, the process follows the chemical equations below:\n", + "\n", + "**CH4 + H2O \u2192 CO + 3H2** \n", + "**CO + H2O \u2192 CO2 + H2**\n", + "\n", + "However, the GibbsReactor unit model solves the equilibrium by minimizing Gibbs free energy. Conveniently, this eliminates the need for a reaction package although a thermophysical package is still required.\n", + "\n", + "The flowsheet that we will be using for this module is shown below with the stream conditions. As in the prior example, we will be processing natural gas and steam feeds of fixed composition to produce hydrogen. The process consists of a mixer M101 for the two inlet streams, a compressor to compress the feed to the reaction pressure, a heater H101 to heat the feed to the reaction temperature, and a GibbsReactor unit R101. We will use thermophysical properties following the Peng-Robinsion cubic equation of state for this flowsheet.\n", + "\n", + "The state variables chosen for the property package are **total molar flows of each stream, temperature of each stream and pressure of each stream, and mole fractions of each component in each stream**. The components considered are: **CH4, H2O, CO, CO2, and H2** and the process occurs in vapor phase only. Therefore, every stream has 1 flow variable, 5 mole fraction variables, 1 temperature and 1 pressure variable. \n", + "\n", + "![](msr_flowsheet.png)\n" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we need to initialize the each unit operation and propagate the outlet results in sequence to solve the flowsheet:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Pyomo and IDAES Components\n", + "\n", + "\n", + "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages, as well as some utility tools to build the flowsheet. For further details on these components, please refer to the [Pyomo documentation]( https://pyomo.readthedocs.io/en/stable/).\n", + "\n", + "From IDAES, we will be needing the `FlowsheetBlock` and the following unit models:\n", + "- Feed\n", + "- Mixer\n", + "- Compressor\n", + "- Heater\n", + "- GibbsReactor\n", + "- Product" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.CH4.properties: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from pyomo.environ import (\n", + " Constraint,\n", + " Var,\n", + " ConcreteModel,\n", + " Expression,\n", + " Objective,\n", + " TransformationFactory,\n", + " value,\n", + " units as pyunits,\n", + ")\n", + "from pyomo.network import Arc\n", + "\n", + "from idaes.core import FlowsheetBlock\n", + "from idaes.models.properties.modular_properties import GenericParameterBlock\n", + "from idaes.models.unit_models import (\n", + " Feed,\n", + " Mixer,\n", + " Compressor,\n", + " Heater,\n", + " GibbsReactor,\n", + " Product,\n", + ")\n", + "\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.util.initialization import propagate_state" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.CH4.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Thermophysical Package\n", + "\n", + "As mentioned earlier, the `GibbsReactor` does not require a reaction package.\n", + "\n", + "Let us import the following module from the IDAES library:\n", + "- natural_gas_PR as get_prop (method to get configuration dictionary)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.CH4.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from idaes.models_extra.power_generation.properties.natural_gas_PR import get_prop" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.CH4: Initialization Complete.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constructing the Flowsheet\n", + "\n", + "Let us create a `ConcreteModel` and add the flowsheet block. " + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H2O.properties: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "m = ConcreteModel()\n", + "m.fs = FlowsheetBlock(dynamic=False)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H2O.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to add the property packages to the flowsheet. Unlike the previous example, we do not need to add a reaction package for the reactor model to calculate results. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) to build a state block for the parameter dictionary." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H2O.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 5, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "thermo_props_config_dict = get_prop(components=[\"CH4\", \"H2O\", \"H2\", \"CO\", \"CO2\"])\n", + "m.fs.thermo_params = GenericParameterBlock(**thermo_props_config_dict)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H2O: Initialization Complete.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Unit Models\n", + "\n", + "Let us start adding the unit models we have imported to the flowsheet:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.M101.methane_feed_state: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "m.fs.CH4 = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.H2O = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", + "m.fs.M101 = Mixer(\n", + " property_package=m.fs.thermo_params, inlet_list=[\"methane_feed\", \"steam_feed\"]\n", + ")\n", + "m.fs.H101 = Heater(\n", + " property_package=m.fs.thermo_params,\n", + " has_pressure_change=False,\n", + " has_phase_equilibrium=False,\n", + ")\n", + "m.fs.C101 = Compressor(property_package=m.fs.thermo_params)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.M101.methane_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101 = GibbsReactor(\n", + " property_package=m.fs.thermo_params,\n", + " has_heat_transfer=True,\n", + " has_pressure_change=False,\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.M101.steam_feed_state: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Connecting Unit Models Using Arcs\n", + "\n", + "We have now added all the unit models we need to the flowsheet. Let us connect the unit models by defining and building each `Arc`:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.M101.steam_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.s01 = Arc(source=m.fs.CH4.outlet, destination=m.fs.M101.methane_feed)\n", + "m.fs.s02 = Arc(source=m.fs.H2O.outlet, destination=m.fs.M101.steam_feed)\n", + "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.C101.inlet)\n", + "m.fs.s04 = Arc(source=m.fs.C101.outlet, destination=m.fs.H101.inlet)\n", + "m.fs.s05 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", + "m.fs.s06 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we can use Pyomo's `TransformationFactory` to write the equality constraints on each Arc:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "TransformationFactory(\"network.expand_arcs\").apply_to(m)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Expressions to Compute Operating Costs\n", + "\n", + "For this flowsheet, we are interested in computing hydrogen production in millions of pounds per year, as well as the total costs due to pressurizing, cooling, and heating utilities:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us add `Expressions` to convert the product flow from mol/s to MM lb/year of hydrogen, and to calculate the cooling, heating and compression operating costs. The total operating cost will be the sum of the costs, expressed in \\\\$/year assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.C101.control_volume.properties_in: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.hyd_prod = Expression(\n", + " expr=pyunits.convert(\n", + " m.fs.PROD.inlet.flow_mol[0]\n", + " * m.fs.PROD.inlet.mole_frac_comp[0, \"H2\"]\n", + " * m.fs.thermo_params.H2.mw, # MW defined in properties as kg/mol\n", + " to_units=pyunits.Mlb / pyunits.yr,\n", + " )\n", + ") # converting kg/s to MM lb/year" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.C101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.cooling_cost = Expression(\n", + " expr=0.212e-7 * (m.fs.R101.heat_duty[0])\n", + ") # the reaction is endothermic, so R101 duty is positive\n", + "m.fs.heating_cost = Expression(\n", + " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", + ") # the stream must be heated to T_rxn, so H101 duty is positive\n", + "m.fs.compression_cost = Expression(\n", + " expr=0.12e-5 * m.fs.C101.work_isentropic[0]\n", + ") # the stream must be pressurized, so the C101 work is positive\n", + "m.fs.operating_cost = Expression(\n", + " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost + m.fs.compression_cost))\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.C101.control_volume.properties_out: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Feed Conditions\n", + "\n", + "We expect each stream to have 8 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the compressor to have 2 (the pressure change and efficiency), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 1 (conversion). Therefore, we have 20 degrees of freedom to specify: temperature, pressure, flow and mole fractions of all five components on both streams; compressor pressure change and efficiency; outlet heater temperature; and reactor conversion." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 12, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "20\n" + ] + } + ], + "source": [ + "print(degrees_of_freedom(m))" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Based on the literature source, we will initialize our simulation with the following values:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.C101.properties_isentropic: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.CH4.outlet.mole_frac_comp[0, \"CH4\"].fix(1)\n", + "m.fs.CH4.outlet.mole_frac_comp[0, \"H2O\"].fix(1e-5)\n", + "m.fs.CH4.outlet.mole_frac_comp[0, \"H2\"].fix(1e-5)\n", + "m.fs.CH4.outlet.mole_frac_comp[0, \"CO\"].fix(1e-5)\n", + "m.fs.CH4.outlet.mole_frac_comp[0, \"CO2\"].fix(1e-5)\n", + "m.fs.CH4.outlet.flow_mol.fix(75 * pyunits.mol / pyunits.s)\n", + "m.fs.CH4.outlet.temperature.fix(298.15 * pyunits.K)\n", + "m.fs.CH4.outlet.pressure.fix(1e5 * pyunits.Pa)\n", + "\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"CH4\"].fix(1e-5)\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"H2O\"].fix(1)\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"H2\"].fix(1e-5)\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"CO\"].fix(1e-5)\n", + "m.fs.H2O.outlet.mole_frac_comp[0, \"CO2\"].fix(1e-5)\n", + "m.fs.H2O.outlet.flow_mol.fix(234 * pyunits.mol / pyunits.s)\n", + "m.fs.H2O.outlet.temperature.fix(373.15 * pyunits.K)\n", + "m.fs.H2O.outlet.pressure.fix(1e5 * pyunits.Pa)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.C101.properties_isentropic: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Unit Model Specifications\n", + "\n", + "For the initial problem, let us fix the compressor outlet pressure to 2 bar for now, the efficiency to 0.90 (a common assumption for compressor units), and the heater outlet temperature to 500 K. We will unfix these values later to optimize the flowsheet." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.C101.properties_isentropic: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.C101.outlet.pressure.fix(pyunits.convert(2 * pyunits.bar, to_units=pyunits.Pa))\n", + "m.fs.C101.efficiency_isentropic.fix(0.90)\n", + "m.fs.H101.outlet.temperature.fix(500 * pyunits.K)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.C101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `GibbsReactor` unit model calculates the amount of product and reactant based on the free energy minimization; therefore, we will specify a desired conversion and let the solver determine the reactor duty and heat transfer. For convenience, we will define the reactor conversion as the amount of methane that is converted." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.conversion = Var(\n", + " initialize=0.80, bounds=(0, 1), units=pyunits.dimensionless\n", + ") # fraction\n", + "\n", + "m.fs.R101.conv_constraint = Constraint(\n", + " expr=m.fs.R101.conversion\n", + " * m.fs.R101.inlet.flow_mol[0]\n", + " * m.fs.R101.inlet.mole_frac_comp[0, \"CH4\"]\n", + " == (\n", + " m.fs.R101.inlet.flow_mol[0] * m.fs.R101.inlet.mole_frac_comp[0, \"CH4\"]\n", + " - m.fs.R101.outlet.flow_mol[0] * m.fs.R101.outlet.mole_frac_comp[0, \"CH4\"]\n", + " )\n", + ")\n", + "\n", + "m.fs.R101.conversion.fix(0.80)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n" + ] + } + ], + "source": [ + "print(degrees_of_freedom(m))" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we need to initialize the each unit operation and propagate the outlet results in sequence to solve the flowsheet:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n" - ] + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4.properties: Starting initialization\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4: Initialization Complete.\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.H2O.properties: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.H2O.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.H2O.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.H2O: Initialization Complete.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.methane_feed_state: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.methane_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.steam_feed_state: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.steam_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.properties_isentropic: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.properties_isentropic: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.properties_isentropic: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.C101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" + ] + } + ], + "source": [ + "# Initialize and solve each unit operation\n", + "m.fs.CH4.initialize()\n", + "propagate_state(arc=m.fs.s01)\n", + "\n", + "m.fs.H2O.initialize()\n", + "propagate_state(arc=m.fs.s02)\n", + "\n", + "m.fs.M101.initialize()\n", + "propagate_state(arc=m.fs.s03)\n", + "\n", + "m.fs.C101.initialize()\n", + "propagate_state(arc=m.fs.s04)\n", + "\n", + "m.fs.H101.initialize()\n", + "propagate_state(arc=m.fs.s05)\n", + "\n", + "m.fs.R101.initialize()\n", + "propagate_state(arc=m.fs.s06)\n", + "\n", + "m.fs.PROD.initialize()\n", + "\n", + "# set solver\n", + "solver = get_solver()" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "code", + "execution_count": 20, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 591\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 490\n", + "\n", + "Total number of variables............................: 203\n", + " variables with only lower bounds: 13\n", + " variables with lower and upper bounds: 179\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 203\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.49e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 1.35e+04 2.00e-01 -1.0 3.59e+00 - 9.90e-01 9.91e-01h 1\n", + " 2 0.0000000e+00 3.59e-04 9.99e+00 -1.0 3.56e+00 - 9.90e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 2.60e-08 8.98e+01 -1.0 2.91e-04 - 9.90e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 3\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 7.6029602259665645e-13 2.5960616767406464e-08\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 7.6029602259665645e-13 2.5960616767406464e-08\n", + "\n", + "\n", + "Number of objective function evaluations = 4\n", + "Number of objective gradient evaluations = 4\n", + "Number of equality constraint evaluations = 4\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 4\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 3\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], + "source": [ + "# Solve the model\n", + "results = solver.solve(m, tee=True)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyze the Results of the Square Problem\n", + "\n", + "\n", + "What is the total operating cost? " + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $39.958 million per year\n" + ] + } + ], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this operating cost, what conversion did we achieve of methane to hydrogen?" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 1.7819e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 429.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.034965\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.32532\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.49984\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.059609\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.080265\n", + " Temperature kelvin 500.00 920.80\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Conversion achieved = 80.0%\n" + ] + } + ], + "source": [ + "m.fs.R101.report()\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:35 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimizing Hydrogen Production\n", + "\n", + "Now that the flowsheet has been squared and solved, we can run a small optimization problem to determine optimal conditions for producing hydrogen. Suppose we wish to find ideal conditions for the competing reactions. The GibbsReactor does not drive equilibrium forward based on temperature, so we will see small amounts of intermediate components present in the product stream. We will allow for variable reactor temperature and pressure by freeing our heater and compressor specifications, and minimize cost to achieve 90% methane conversion. Since we assume an isentopic compressor, allowing compression will heat our feed stream and reduce or eliminate the required heater duty." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:36 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us declare our objective function for this problem. " + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:36 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.objective = Objective(expr=m.fs.operating_cost)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:36 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem until now, as well as set bounds for the design variables (reactor outlet temperature is set by state variable bounds in property package):" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:36 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.conversion.fix(0.90)\n", + "\n", + "m.fs.C101.outlet.pressure.unfix()\n", + "m.fs.C101.outlet.pressure[0].setlb(\n", + " pyunits.convert(1 * pyunits.bar, to_units=pyunits.Pa)\n", + ") # equals inlet pressure\n", + "m.fs.C101.outlet.pressure[0].setlb(\n", + " pyunits.convert(10 * pyunits.bar, to_units=pyunits.Pa)\n", + ") # at most, pressurize to 1 bar\n", + "\n", + "m.fs.H101.outlet.temperature.unfix()\n", + "m.fs.H101.heat_duty[0].setlb(\n", + " 0 * pyunits.J / pyunits.s\n", + ") # ensures outlet is equal to or greater than inlet temperature\n", + "m.fs.H101.outlet.temperature[0].setub(1000 * pyunits.K) # at most, heat to 1000 K" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:36 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" - ] - } - ], - "source": [ - "# Initialize and solve each unit operation\n", - "m.fs.CH4.initialize()\n", - "propagate_state(arc=m.fs.s01)\n", - "\n", - "m.fs.H2O.initialize()\n", - "propagate_state(arc=m.fs.s02)\n", - "\n", - "m.fs.M101.initialize()\n", - "propagate_state(arc=m.fs.s03)\n", - "\n", - "m.fs.C101.initialize()\n", - "propagate_state(arc=m.fs.s04)\n", - "\n", - "m.fs.H101.initialize()\n", - "propagate_state(arc=m.fs.s05)\n", - "\n", - "m.fs.R101.initialize()\n", - "propagate_state(arc=m.fs.s06)\n", - "\n", - "m.fs.PROD.initialize()\n", - "\n", - "# set solver\n", - "solver = get_solver()" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "scrolled": true - }, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "We have now defined the optimization problem and we are now ready to solve this problem. \n", + "\n", + "\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 591\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 490\n", - "\n", - "Total number of variables............................: 203\n", - " variables with only lower bounds: 13\n", - " variables with lower and upper bounds: 179\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 203\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.49e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 1.35e+04 2.00e-01 -1.0 3.59e+00 - 9.90e-01 9.91e-01h 1\n", - " 2 0.0000000e+00 3.59e-04 9.99e+00 -1.0 3.56e+00 - 9.90e-01 1.00e+00h 1\n", - " 3 0.0000000e+00 1.75e-08 8.98e+01 -1.0 2.91e-04 - 9.90e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 3\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 1.1641532182693481e-10 1.7462298274040222e-08\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.1641532182693481e-10 1.7462298274040222e-08\n", - "\n", - "\n", - "Number of objective function evaluations = 4\n", - "Number of objective gradient evaluations = 4\n", - "Number of equality constraint evaluations = 4\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 4\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 3\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "# Solve the model\n", - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Analyze the Results of the Square Problem\n", - "\n", - "\n", - "What is the total operating cost? " - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 29, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 598\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 506\n", + "\n", + "Total number of variables............................: 205\n", + " variables with only lower bounds: 14\n", + " variables with lower and upper bounds: 181\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 203\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 3.9958388e+07 1.49e+06 3.46e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 3.8920063e+07 1.48e+06 1.52e+03 -1.0 7.19e+06 - 3.91e-01 6.43e-03f 1\n", + " 2 7.0948609e+07 1.15e+06 1.86e+06 -1.0 4.83e+06 - 1.51e-01 2.26e-01h 1\n", + " 3 1.0553921e+08 5.23e+05 1.04e+07 -1.0 2.42e+06 - 3.41e-01 5.67e-01h 1\n", + " 4 1.0874890e+08 1.58e+05 7.64e+06 -1.0 8.45e+05 - 7.09e-01 7.11e-01h 1\n", + " 5 1.0751027e+08 1.51e+04 1.67e+06 -1.0 2.97e+05 - 9.49e-01 9.09e-01f 1\n", + " 6 1.0721898e+08 5.95e+00 9.98e+03 -1.0 3.47e+04 - 9.90e-01 1.00e+00f 1\n", + " 7 1.0721794e+08 3.43e-05 8.84e+01 -1.0 1.59e+02 - 9.90e-01 1.00e+00f 1\n", + " 8 1.0721794e+08 1.90e-08 7.14e-01 -1.0 1.43e-02 - 9.92e-01 1.00e+00h 1\n", + " 9 1.0721794e+08 7.55e-09 1.53e-06 -2.5 1.72e-02 - 1.00e+00 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 1.0721794e+08 2.10e-08 1.59e-06 -3.8 4.73e-04 - 1.00e+00 1.00e+00f 1\n", + " 11 1.0721794e+08 1.12e-08 2.07e-06 -5.7 2.63e-05 - 1.00e+00 1.00e+00f 1\n", + " 12 1.0721794e+08 3.57e-08 1.65e-06 -7.0 3.14e-07 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 12\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 1.0721793780338226e+08 1.0721793780338226e+08\n", + "Dual infeasibility......: 1.6485091371912918e-06 1.6485091371912918e-06\n", + "Constraint violation....: 4.6566128730773926e-10 3.5680419252624450e-08\n", + "Complementarity.........: 9.0909090914354020e-08 9.0909090914354020e-08\n", + "Overall NLP error.......: 9.0909090914354020e-08 1.6485091371912918e-06\n", + "\n", + "\n", + "Number of objective function evaluations = 13\n", + "Number of objective gradient evaluations = 13\n", + "Number of equality constraint evaluations = 13\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 13\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in NLP function evaluations = 0.011\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], + "source": [ + "results = solver.solve(m, tee=True)" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $39.958 million per year\n" - ] - } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For this operating cost, what conversion did we achieve of methane to hydrogen?" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $107.218 million per year\n", + "\n", + "Compressor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.C101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Isentropic Efficiency : 0.90000 : dimensionless : True : (None, None)\n", + " Mechanical Work : 3.0334e+06 : watt : False : (None, None)\n", + " Pressure Change : 9.0000e+05 : pascal : False : (None, None)\n", + " Pressure Ratio : 10.000 : dimensionless : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 353.80 619.25\n", + " Pressure pascal 1.0000e+05 1.0000e+06\n", + "====================================================================================\n", + "\n", + "Heater results\n", + "\n", + "====================================================================================\n", + "Unit : fs.H101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 5.8781e-09 : watt : False : (0.0, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 619.25 619.25\n", + " Pressure pascal 1.0000e+06 1.0000e+06\n", + "====================================================================================\n", + "\n", + "Gibbs reactor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 2.1076e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 444.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.016892\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.31609\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.51498\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.093140\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.058900\n", + " Temperature kelvin 619.25 1087.4\n", + " Pressure pascal 1.0000e+06 1.0000e+06\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "\n", + "print()\n", + "print(\"Compressor results\")\n", + "\n", + "m.fs.C101.report()\n", + "\n", + "print()\n", + "print(\"Heater results\")\n", + "\n", + "m.fs.H101.report()\n", + "\n", + "print()\n", + "print(\"Gibbs reactor results\")\n", + "\n", + "m.fs.R101.report()" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "====================================================================================\n", - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : 1.7819e+07 : watt : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Total Molar Flowrate mole / second 309.01 429.02\n", - " Total Mole Fraction CH4 dimensionless 0.24272 0.034965\n", - " Total Mole Fraction H2O dimensionless 0.75725 0.32532\n", - " Total Mole Fraction H2 dimensionless 9.9996e-06 0.49984\n", - " Total Mole Fraction CO dimensionless 9.9996e-06 0.059609\n", - " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.080265\n", - " Temperature kelvin 500.00 920.80\n", - " Pressure pascal 2.0000e+05 2.0000e+05\n", - "====================================================================================\n", - "\n", - "Conversion achieved = 80.0%\n" - ] - } - ], - "source": [ - "m.fs.R101.report()\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Optimizing Hydrogen Production\n", - "\n", - "Now that the flowsheet has been squared and solved, we can run a small optimization problem to determine optimal conditions for producing hydrogen. Suppose we wish to find ideal conditions for the competing reactions. The GibbsReactor does not drive equilibrium forward based on temperature, so we will see small amounts of intermediate components present in the product stream. We will allow for variable reactor temperature and pressure by freeing our heater and compressor specifications, and minimize cost to achieve 90% methane conversion. Since we assume an isentopic compressor, allowing compression will heat our feed stream and reduce or eliminate the required heater duty." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us declare our objective function for this problem. " - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.objective = Objective(expr=m.fs.operating_cost)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem until now, as well as set bounds for the design variables (reactor outlet temperature is set by state variable bounds in property package):" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.R101.conversion.fix(0.90)\n", - "\n", - "m.fs.C101.outlet.pressure.unfix()\n", - "m.fs.C101.outlet.pressure[0].setlb(\n", - " pyunits.convert(1 * pyunits.bar, to_units=pyunits.Pa)\n", - ") # equals inlet pressure\n", - "m.fs.C101.outlet.pressure[0].setlb(\n", - " pyunits.convert(10 * pyunits.bar, to_units=pyunits.Pa)\n", - ") # at most, pressurize to 1 bar\n", - "\n", - "m.fs.H101.outlet.temperature.unfix()\n", - "m.fs.H101.heat_duty[0].setlb(\n", - " 0 * pyunits.J / pyunits.s\n", - ") # ensures outlet is equal to or greater than inlet temperature\n", - "m.fs.H101.outlet.temperature[0].setub(1000 * pyunits.K) # at most, heat to 1000 K" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "We have now defined the optimization problem and we are now ready to solve this problem. \n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "scrolled": true - }, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display optimal values for the decision variables and design variables:" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 598\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 506\n", - "\n", - "Total number of variables............................: 205\n", - " variables with only lower bounds: 14\n", - " variables with lower and upper bounds: 181\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 203\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 3.9958388e+07 1.49e+06 3.46e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 3.8920063e+07 1.48e+06 1.52e+03 -1.0 7.19e+06 - 3.91e-01 6.43e-03f 1\n", - " 2 7.0948609e+07 1.15e+06 1.86e+06 -1.0 4.83e+06 - 1.51e-01 2.26e-01h 1\n", - " 3 1.0553921e+08 5.23e+05 1.04e+07 -1.0 2.42e+06 - 3.41e-01 5.67e-01h 1\n", - " 4 1.0874890e+08 1.58e+05 7.64e+06 -1.0 8.45e+05 - 7.09e-01 7.11e-01h 1\n", - " 5 1.0751027e+08 1.51e+04 1.67e+06 -1.0 2.97e+05 - 9.49e-01 9.09e-01f 1\n", - " 6 1.0721898e+08 5.95e+00 9.98e+03 -1.0 3.47e+04 - 9.90e-01 1.00e+00f 1\n", - " 7 1.0721794e+08 3.43e-05 8.84e+01 -1.0 1.59e+02 - 9.90e-01 1.00e+00f 1\n", - " 8 1.0721794e+08 8.85e-09 7.14e-01 -1.0 1.43e-02 - 9.92e-01 1.00e+00h 1\n", - " 9 1.0721794e+08 1.50e-08 1.92e-06 -2.5 1.72e-02 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 1.0721794e+08 1.02e-08 2.70e-06 -3.8 4.73e-04 - 1.00e+00 1.00e+00f 1\n", - " 11 1.0721794e+08 1.02e-08 2.31e-06 -5.7 2.63e-05 - 1.00e+00 1.00e+00f 1\n", - " 12 1.0721794e+08 1.49e-08 1.00e-06 -7.0 3.06e-07 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 12\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 1.0721793780338201e+08 1.0721793780338201e+08\n", - "Dual infeasibility......: 1.0011035875619890e-06 1.0011035875619890e-06\n", - "Constraint violation....: 1.5205920451933321e-12 1.4901161193847656e-08\n", - "Complementarity.........: 9.0909090914354020e-08 9.0909090914354020e-08\n", - "Overall NLP error.......: 9.0909090914354020e-08 1.0011035875619890e-06\n", - "\n", - "\n", - "Number of objective function evaluations = 13\n", - "Number of objective gradient evaluations = 13\n", - "Number of equality constraint evaluations = 13\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 13\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 12\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", - "Total CPU secs in NLP function evaluations = 0.004\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimal Values\n", + "\n", + "C101 outlet pressure = 1.000 MPa\n", + "\n", + "C101 outlet temperature = 619.248 K\n", + "\n", + "H101 outlet temperature = 619.248 K\n", + "\n", + "R101 outlet temperature = 1087.385 K\n", + "\n", + "Hydrogen produced = 32.070 MM lb/year\n", + "\n", + "Conversion achieved = 90.0%\n" + ] + } + ], + "source": [ + "print(\"Optimal Values\")\n", + "print()\n", + "\n", + "print(f\"C101 outlet pressure = {value(m.fs.C101.outlet.pressure[0])/1E6:0.3f} MPa\")\n", + "print()\n", + "\n", + "print(f\"C101 outlet temperature = {value(m.fs.C101.outlet.temperature[0]):0.3f} K\")\n", + "print()\n", + "\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "\n", + "print()\n", + "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.3f} K\")\n", + "\n", + "print()\n", + "print(f\"Hydrogen produced = {value(m.fs.hyd_prod):0.3f} MM lb/year\")\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $107.218 million per year\n", - "\n", - "Compressor results\n", - "\n", - "====================================================================================\n", - "Unit : fs.C101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Isentropic Efficiency : 0.90000 : dimensionless : True : (None, None)\n", - " Mechanical Work : 3.0334e+06 : watt : False : (None, None)\n", - " Pressure Change : 9.0000e+05 : pascal : False : (None, None)\n", - " Pressure Ratio : 10.000 : dimensionless : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Total Molar Flowrate mole / second 309.01 309.01\n", - " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", - " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", - " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", - " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", - " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", - " Temperature kelvin 353.80 619.25\n", - " Pressure pascal 1.0000e+05 1.0000e+06\n", - "====================================================================================\n", - "\n", - "Heater results\n", - "\n", - "====================================================================================\n", - "Unit : fs.H101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : 5.8781e-09 : watt : False : (0.0, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Total Molar Flowrate mole / second 309.01 309.01\n", - " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", - " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", - " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", - " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", - " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", - " Temperature kelvin 619.25 619.25\n", - " Pressure pascal 1.0000e+06 1.0000e+06\n", - "====================================================================================\n", - "\n", - "Gibbs reactor results\n", - "\n", - "====================================================================================\n", - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : 2.1076e+07 : watt : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Total Molar Flowrate mole / second 309.01 444.02\n", - " Total Mole Fraction CH4 dimensionless 0.24272 0.016892\n", - " Total Mole Fraction H2O dimensionless 0.75725 0.31609\n", - " Total Mole Fraction H2 dimensionless 9.9996e-06 0.51498\n", - " Total Mole Fraction CO dimensionless 9.9996e-06 0.093140\n", - " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.058900\n", - " Temperature kelvin 619.25 1087.4\n", - " Pressure pascal 1.0000e+06 1.0000e+06\n", - "====================================================================================\n" - ] + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", - "\n", - "print()\n", - "print(\"Compressor results\")\n", - "\n", - "m.fs.C101.report()\n", - "\n", - "print()\n", - "print(\"Heater results\")\n", - "\n", - "m.fs.H101.report()\n", - "\n", - "print()\n", - "print(\"Gibbs reactor results\")\n", - "\n", - "m.fs.R101.report()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Display optimal values for the decision variables and design variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimal Values\n", - "\n", - "C101 outlet pressure = 1.000 MPa\n", - "\n", - "C101 outlet temperature = 619.248 K\n", - "\n", - "H101 outlet temperature = 619.248 K\n", - "\n", - "R101 outlet temperature = 1087.385 K\n", - "\n", - "Hydrogen produced = 32.070 MM lb/year\n", - "\n", - "Conversion achieved = 90.0%\n" - ] + ], + "metadata": { + "celltoolbar": "Tags", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" } - ], - "source": [ - "print(\"Optimal Values\")\n", - "print()\n", - "\n", - "print(f\"C101 outlet pressure = {value(m.fs.C101.outlet.pressure[0])/1E6:0.3f} MPa\")\n", - "print()\n", - "\n", - "print(f\"C101 outlet temperature = {value(m.fs.C101.outlet.temperature[0]):0.3f} K\")\n", - "print()\n", - "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", - "\n", - "print()\n", - "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.3f} K\")\n", - "\n", - "print()\n", - "print(f\"Hydrogen produced = {value(m.fs.hyd_prod):0.3f} MM lb/year\")\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "celltoolbar": "Tags", - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 3 + "nbformat": 4, + "nbformat_minor": 3 } \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_test.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_test.ipynb index 21f63514..7a9cba50 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_test.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_test.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "tags": [ "header", @@ -61,8 +61,7 @@ "\n", "The state variables chosen for the property package are **total molar flows of each stream, temperature of each stream and pressure of each stream, and mole fractions of each component in each stream**. The components considered are: **CH4, H2O, CO, CO2, and H2** and the process occurs in vapor phase only. Therefore, every stream has 1 flow variable, 5 mole fraction variables, 1 temperature and 1 pressure variable. \n", "\n", - "![](msr_flowsheet.png)\n", - "" + "![](msr_flowsheet.png)\n" ] }, { @@ -85,7 +84,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -131,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -149,7 +148,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -166,7 +165,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "scrolled": true }, @@ -187,7 +186,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "scrolled": true }, @@ -209,10 +208,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, + "execution_count": 7, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = GibbsReactor(\n", @@ -233,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -254,7 +251,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -279,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -295,7 +292,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -324,18 +321,26 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "20\n" + ] + } + ], "source": [ "print(degrees_of_freedom(m))" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 13, "metadata": { "tags": [ "testing" @@ -356,7 +361,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -390,7 +395,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -408,7 +413,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -438,16 +443,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n" + ] + } + ], "source": [ "print(degrees_of_freedom(m))" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": { "tags": [ "testing" @@ -468,9 +481,57 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4.properties: Starting initialization\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4: Initialization Complete.\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.H2O.properties: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.H2O.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.H2O.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.H2O: Initialization Complete.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.methane_feed_state: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.methane_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.steam_feed_state: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.steam_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.properties_isentropic: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.properties_isentropic: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.properties_isentropic: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.C101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" + ] + } + ], "source": [ "# Initialize and solve each unit operation\n", "m.fs.CH4.initialize()\n", @@ -499,11 +560,84 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 591\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 490\n", + "\n", + "Total number of variables............................: 203\n", + " variables with only lower bounds: 13\n", + " variables with lower and upper bounds: 179\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 203\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.49e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 1.35e+04 2.00e-01 -1.0 3.59e+00 - 9.90e-01 9.91e-01h 1\n", + " 2 0.0000000e+00 3.59e-04 9.99e+00 -1.0 3.56e+00 - 9.90e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 2.60e-08 8.98e+01 -1.0 2.91e-04 - 9.90e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 3\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 7.6029602259665645e-13 2.5960616767406464e-08\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 7.6029602259665645e-13 2.5960616767406464e-08\n", + "\n", + "\n", + "Number of objective function evaluations = 4\n", + "Number of objective gradient evaluations = 4\n", + "Number of equality constraint evaluations = 4\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 4\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 3\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], "source": [ "# Solve the model\n", "results = solver.solve(m, tee=True)" @@ -511,7 +645,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": { "tags": [ "testing" @@ -537,16 +671,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $39.958 million per year\n" + ] + } + ], "source": [ "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": { "tags": [ "testing" @@ -568,9 +710,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 1.7819e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 429.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.034965\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.32532\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.49984\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.059609\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.080265\n", + " Temperature kelvin 500.00 920.80\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Conversion achieved = 80.0%\n" + ] + } + ], "source": [ "m.fs.R101.report()\n", "\n", @@ -580,7 +754,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": { "tags": [ "testing" @@ -611,7 +785,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ @@ -627,7 +801,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -650,7 +824,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "metadata": { "tags": [ "testing" @@ -673,18 +847,101 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 598\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 506\n", + "\n", + "Total number of variables............................: 205\n", + " variables with only lower bounds: 14\n", + " variables with lower and upper bounds: 181\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 203\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 3.9958388e+07 1.49e+06 3.46e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 3.8920063e+07 1.48e+06 1.52e+03 -1.0 7.19e+06 - 3.91e-01 6.43e-03f 1\n", + " 2 7.0948609e+07 1.15e+06 1.86e+06 -1.0 4.83e+06 - 1.51e-01 2.26e-01h 1\n", + " 3 1.0553921e+08 5.23e+05 1.04e+07 -1.0 2.42e+06 - 3.41e-01 5.67e-01h 1\n", + " 4 1.0874890e+08 1.58e+05 7.64e+06 -1.0 8.45e+05 - 7.09e-01 7.11e-01h 1\n", + " 5 1.0751027e+08 1.51e+04 1.67e+06 -1.0 2.97e+05 - 9.49e-01 9.09e-01f 1\n", + " 6 1.0721898e+08 5.95e+00 9.98e+03 -1.0 3.47e+04 - 9.90e-01 1.00e+00f 1\n", + " 7 1.0721794e+08 3.43e-05 8.84e+01 -1.0 1.59e+02 - 9.90e-01 1.00e+00f 1\n", + " 8 1.0721794e+08 1.90e-08 7.14e-01 -1.0 1.43e-02 - 9.92e-01 1.00e+00h 1\n", + " 9 1.0721794e+08 7.55e-09 1.53e-06 -2.5 1.72e-02 - 1.00e+00 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 1.0721794e+08 2.10e-08 1.59e-06 -3.8 4.73e-04 - 1.00e+00 1.00e+00f 1\n", + " 11 1.0721794e+08 1.12e-08 2.07e-06 -5.7 2.63e-05 - 1.00e+00 1.00e+00f 1\n", + " 12 1.0721794e+08 3.57e-08 1.65e-06 -7.0 3.14e-07 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 12\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 1.0721793780338226e+08 1.0721793780338226e+08\n", + "Dual infeasibility......: 1.6485091371912918e-06 1.6485091371912918e-06\n", + "Constraint violation....: 4.6566128730773926e-10 3.5680419252624450e-08\n", + "Complementarity.........: 9.0909090914354020e-08 9.0909090914354020e-08\n", + "Overall NLP error.......: 9.0909090914354020e-08 1.6485091371912918e-06\n", + "\n", + "\n", + "Number of objective function evaluations = 13\n", + "Number of objective gradient evaluations = 13\n", + "Number of equality constraint evaluations = 13\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 13\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in NLP function evaluations = 0.011\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], "source": [ "results = solver.solve(m, tee=True)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "metadata": { "tags": [ "testing" @@ -700,9 +957,95 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $107.218 million per year\n", + "\n", + "Compressor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.C101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Isentropic Efficiency : 0.90000 : dimensionless : True : (None, None)\n", + " Mechanical Work : 3.0334e+06 : watt : False : (None, None)\n", + " Pressure Change : 9.0000e+05 : pascal : False : (None, None)\n", + " Pressure Ratio : 10.000 : dimensionless : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 353.80 619.25\n", + " Pressure pascal 1.0000e+05 1.0000e+06\n", + "====================================================================================\n", + "\n", + "Heater results\n", + "\n", + "====================================================================================\n", + "Unit : fs.H101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 5.8781e-09 : watt : False : (0.0, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 619.25 619.25\n", + " Pressure pascal 1.0000e+06 1.0000e+06\n", + "====================================================================================\n", + "\n", + "Gibbs reactor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 2.1076e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 444.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.016892\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.31609\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.51498\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.093140\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.058900\n", + " Temperature kelvin 619.25 1087.4\n", + " Pressure pascal 1.0000e+06 1.0000e+06\n", + "====================================================================================\n" + ] + } + ], "source": [ "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", "\n", @@ -724,7 +1067,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 32, "metadata": { "tags": [ "testing" @@ -744,9 +1087,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimal Values\n", + "\n", + "C101 outlet pressure = 1.000 MPa\n", + "\n", + "C101 outlet temperature = 619.248 K\n", + "\n", + "H101 outlet temperature = 619.248 K\n", + "\n", + "R101 outlet temperature = 1087.385 K\n", + "\n", + "Hydrogen produced = 32.070 MM lb/year\n", + "\n", + "Conversion achieved = 90.0%\n" + ] + } + ], "source": [ "print(\"Optimal Values\")\n", "print()\n", @@ -771,7 +1134,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 34, "metadata": { "tags": [ "testing" @@ -812,7 +1175,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_usr.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_usr.ipynb index abe0717b..b7984618 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_usr.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/gibbs_reactor_usr.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "tags": [ "header", @@ -61,8 +61,7 @@ "\n", "The state variables chosen for the property package are **total molar flows of each stream, temperature of each stream and pressure of each stream, and mole fractions of each component in each stream**. The components considered are: **CH4, H2O, CO, CO2, and H2** and the process occurs in vapor phase only. Therefore, every stream has 1 flow variable, 5 mole fraction variables, 1 temperature and 1 pressure variable. \n", "\n", - "![](msr_flowsheet.png)\n", - "" + "![](msr_flowsheet.png)\n" ] }, { @@ -85,7 +84,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -131,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -149,7 +148,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -166,7 +165,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "scrolled": true }, @@ -187,7 +186,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": { "scrolled": true }, @@ -209,10 +208,8 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, + "execution_count": 7, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = GibbsReactor(\n", @@ -233,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -254,7 +251,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -279,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -295,7 +292,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -324,11 +321,19 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "20\n" + ] + } + ], "source": [ "print(degrees_of_freedom(m))" ] @@ -342,7 +347,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -376,7 +381,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, "outputs": [], "source": [ @@ -394,7 +399,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -424,9 +429,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n" + ] + } + ], "source": [ "print(degrees_of_freedom(m))" ] @@ -440,9 +453,57 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4.properties: Starting initialization\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.CH4: Initialization Complete.\n", + "2024-05-23 06:07:53 [INFO] idaes.init.fs.H2O.properties: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.H2O.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.H2O.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.H2O: Initialization Complete.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.methane_feed_state: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.methane_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.steam_feed_state: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.steam_feed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.control_volume.properties_out: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.properties_isentropic: Starting initialization\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.properties_isentropic: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:54 [INFO] idaes.init.fs.C101.properties_isentropic: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.C101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:55 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n", + "2024-05-23 06:07:56 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" + ] + } + ], "source": [ "# Initialize and solve each unit operation\n", "m.fs.CH4.initialize()\n", @@ -471,11 +532,84 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 591\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 490\n", + "\n", + "Total number of variables............................: 203\n", + " variables with only lower bounds: 13\n", + " variables with lower and upper bounds: 179\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 203\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 0.0000000e+00 1.49e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 1.35e+04 2.00e-01 -1.0 3.59e+00 - 9.90e-01 9.91e-01h 1\n", + " 2 0.0000000e+00 3.59e-04 9.99e+00 -1.0 3.56e+00 - 9.90e-01 1.00e+00h 1\n", + " 3 0.0000000e+00 2.60e-08 8.98e+01 -1.0 2.91e-04 - 9.90e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 3\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 7.6029602259665645e-13 2.5960616767406464e-08\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 7.6029602259665645e-13 2.5960616767406464e-08\n", + "\n", + "\n", + "Number of objective function evaluations = 4\n", + "Number of objective gradient evaluations = 4\n", + "Number of equality constraint evaluations = 4\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 4\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 3\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], "source": [ "# Solve the model\n", "results = solver.solve(m, tee=True)" @@ -493,9 +627,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $39.958 million per year\n" + ] + } + ], "source": [ "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" ] @@ -509,9 +651,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 1.7819e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 429.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.034965\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.32532\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.49984\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.059609\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.080265\n", + " Temperature kelvin 500.00 920.80\n", + " Pressure pascal 2.0000e+05 2.0000e+05\n", + "====================================================================================\n", + "\n", + "Conversion achieved = 80.0%\n" + ] + } + ], "source": [ "m.fs.R101.report()\n", "\n", @@ -537,7 +711,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ @@ -553,7 +727,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "metadata": {}, "outputs": [], "source": [ @@ -586,20 +760,189 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 29, "metadata": { "scrolled": true }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", + "tol=1e-06\n", + "max_iter=200\n", + "\n", + "\n", + "******************************************************************************\n", + "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", + " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", + " For more information visit http://projects.coin-or.org/Ipopt\n", + "\n", + "This version of Ipopt was compiled from source code available at\n", + " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", + " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", + " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", + "\n", + "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", + " for large-scale scientific computation. All technical papers, sales and\n", + " publicity material resulting from use of the HSL codes within IPOPT must\n", + " contain the following acknowledgement:\n", + " HSL, a collection of Fortran codes for large-scale scientific\n", + " computation. See http://www.hsl.rl.ac.uk.\n", + "******************************************************************************\n", + "\n", + "This is Ipopt version 3.13.2, running with linear solver ma27.\n", + "\n", + "Number of nonzeros in equality constraint Jacobian...: 598\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 506\n", + "\n", + "Total number of variables............................: 205\n", + " variables with only lower bounds: 14\n", + " variables with lower and upper bounds: 181\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 203\n", + "Total number of inequality constraints...............: 0\n", + " inequality constraints with only lower bounds: 0\n", + " inequality constraints with lower and upper bounds: 0\n", + " inequality constraints with only upper bounds: 0\n", + "\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 0 3.9958388e+07 1.49e+06 3.46e+01 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 3.8920063e+07 1.48e+06 1.52e+03 -1.0 7.19e+06 - 3.91e-01 6.43e-03f 1\n", + " 2 7.0948609e+07 1.15e+06 1.86e+06 -1.0 4.83e+06 - 1.51e-01 2.26e-01h 1\n", + " 3 1.0553921e+08 5.23e+05 1.04e+07 -1.0 2.42e+06 - 3.41e-01 5.67e-01h 1\n", + " 4 1.0874890e+08 1.58e+05 7.64e+06 -1.0 8.45e+05 - 7.09e-01 7.11e-01h 1\n", + " 5 1.0751027e+08 1.51e+04 1.67e+06 -1.0 2.97e+05 - 9.49e-01 9.09e-01f 1\n", + " 6 1.0721898e+08 5.95e+00 9.98e+03 -1.0 3.47e+04 - 9.90e-01 1.00e+00f 1\n", + " 7 1.0721794e+08 3.43e-05 8.84e+01 -1.0 1.59e+02 - 9.90e-01 1.00e+00f 1\n", + " 8 1.0721794e+08 1.90e-08 7.14e-01 -1.0 1.43e-02 - 9.92e-01 1.00e+00h 1\n", + " 9 1.0721794e+08 7.55e-09 1.53e-06 -2.5 1.72e-02 - 1.00e+00 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10 1.0721794e+08 2.10e-08 1.59e-06 -3.8 4.73e-04 - 1.00e+00 1.00e+00f 1\n", + " 11 1.0721794e+08 1.12e-08 2.07e-06 -5.7 2.63e-05 - 1.00e+00 1.00e+00f 1\n", + " 12 1.0721794e+08 3.57e-08 1.65e-06 -7.0 3.14e-07 - 1.00e+00 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 12\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 1.0721793780338226e+08 1.0721793780338226e+08\n", + "Dual infeasibility......: 1.6485091371912918e-06 1.6485091371912918e-06\n", + "Constraint violation....: 4.6566128730773926e-10 3.5680419252624450e-08\n", + "Complementarity.........: 9.0909090914354020e-08 9.0909090914354020e-08\n", + "Overall NLP error.......: 9.0909090914354020e-08 1.6485091371912918e-06\n", + "\n", + "\n", + "Number of objective function evaluations = 13\n", + "Number of objective gradient evaluations = 13\n", + "Number of equality constraint evaluations = 13\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 13\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in NLP function evaluations = 0.011\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + } + ], "source": [ "results = solver.solve(m, tee=True)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 31, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "operating cost = $107.218 million per year\n", + "\n", + "Compressor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.C101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Isentropic Efficiency : 0.90000 : dimensionless : True : (None, None)\n", + " Mechanical Work : 3.0334e+06 : watt : False : (None, None)\n", + " Pressure Change : 9.0000e+05 : pascal : False : (None, None)\n", + " Pressure Ratio : 10.000 : dimensionless : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 353.80 619.25\n", + " Pressure pascal 1.0000e+05 1.0000e+06\n", + "====================================================================================\n", + "\n", + "Heater results\n", + "\n", + "====================================================================================\n", + "Unit : fs.H101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 5.8781e-09 : watt : False : (0.0, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 309.01\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.24272\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.75725\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 9.9996e-06\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 9.9996e-06\n", + " Temperature kelvin 619.25 619.25\n", + " Pressure pascal 1.0000e+06 1.0000e+06\n", + "====================================================================================\n", + "\n", + "Gibbs reactor results\n", + "\n", + "====================================================================================\n", + "Unit : fs.R101 Time: 0.0\n", + "------------------------------------------------------------------------------------\n", + " Unit Performance\n", + "\n", + " Variables: \n", + "\n", + " Key : Value : Units : Fixed : Bounds\n", + " Heat Duty : 2.1076e+07 : watt : False : (None, None)\n", + "\n", + "------------------------------------------------------------------------------------\n", + " Stream Table\n", + " Units Inlet Outlet \n", + " Total Molar Flowrate mole / second 309.01 444.02\n", + " Total Mole Fraction CH4 dimensionless 0.24272 0.016892\n", + " Total Mole Fraction H2O dimensionless 0.75725 0.31609\n", + " Total Mole Fraction H2 dimensionless 9.9996e-06 0.51498\n", + " Total Mole Fraction CO dimensionless 9.9996e-06 0.093140\n", + " Total Mole Fraction CO2 dimensionless 9.9996e-06 0.058900\n", + " Temperature kelvin 619.25 1087.4\n", + " Pressure pascal 1.0000e+06 1.0000e+06\n", + "====================================================================================\n" + ] + } + ], "source": [ "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", "\n", @@ -628,9 +971,29 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 33, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimal Values\n", + "\n", + "C101 outlet pressure = 1.000 MPa\n", + "\n", + "C101 outlet temperature = 619.248 K\n", + "\n", + "H101 outlet temperature = 619.248 K\n", + "\n", + "R101 outlet temperature = 1087.385 K\n", + "\n", + "Hydrogen produced = 32.070 MM lb/year\n", + "\n", + "Conversion achieved = 90.0%\n" + ] + } + ], "source": [ "print(\"Optimal Values\")\n", "print()\n", @@ -678,7 +1041,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor.ipynb index 2fe06cf1..989a3458 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor.ipynb @@ -626,7 +626,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" ] }, { @@ -641,7 +641,7 @@ "source": [ "import pytest\n", "\n", - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(2.082, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(6.589, rel=1e-5)" ] }, { @@ -667,15 +667,15 @@ " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * \n", " (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", - " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.3f}\"\"\"\n", + " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.6f}\"\"\"\n", " f\" MJ\"\n", ")\n", "print()\n", - "print(f\"Tube area required = {value(m.fs.R101.area):0.3f} m^2\")\n", + "print(f\"Tube area required = {value(m.fs.R101.area):0.6f} m^2\")\n", "print()\n", - "print(f\"Tube length required = {value(m.fs.R101.length):0.3f} m\")\n", + "print(f\"Tube length required = {value(m.fs.R101.length):0.6f} m\")\n", "print()\n", - "print(f\"Tube volume required = {value(m.fs.R101.volume):0.3f} m^3\")" + "print(f\"Tube volume required = {value(m.fs.R101.volume):0.6f} m^3\")" ] }, { @@ -688,8 +688,8 @@ }, "outputs": [], "source": [ - "assert value(m.fs.R101.conversion) == pytest.approx(0.5000, abs=1e-3)\n", - "assert value(m.fs.R101.area) == pytest.approx(1.1490, abs=1e-3)\n", + "assert value(m.fs.R101.conversion) == pytest.approx(0.5000, rel=1e-5)\n", + "assert value(m.fs.R101.area) == pytest.approx(0.987071, rel=1e-5)\n", "assert (\n", " value(m.fs.R101.length)\n", " / value(m.fs.R101.config.finite_elements)\n", @@ -705,8 +705,8 @@ " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)])\n", " )\n", " / 2\n", - ") / 1e6 == pytest.approx(-4.734, abs=1e-3)\n", - "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2815, abs=1e-3)" + ") / 1e6 == pytest.approx(-4.881815, rel=1e-5)\n", + "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2815, rel=1e-5)" ] }, { @@ -829,7 +829,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", "\n", "print()\n", "print(\"Heater results\")\n", @@ -852,8 +852,8 @@ }, "outputs": [], "source": [ - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(0.3184, abs=1e-3)\n", - "assert value(m.fs.R101.area) == pytest.approx(2.0870, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(4.421530, rel=1e-5)\n", + "assert value(m.fs.R101.area) == pytest.approx(2.9300, rel=1e-5)" ] }, { @@ -872,31 +872,31 @@ "print(\"Optimal Values\")\n", "print()\n", "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", "print(\n", " \"Total heat duty required = \",\n", " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", - " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.3f}\"\"\"\n", + " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.6f}\"\"\"\n", " f\" MJ\",\n", ")\n", "print()\n", - "print(f\"Tube area required = {value(m.fs.R101.area):0.3f} m^2\")\n", + "print(f\"Tube area required = {value(m.fs.R101.area):0.6f} m^2\")\n", "\n", "print()\n", - "print(f\"Tube length required = {value(m.fs.R101.length):0.3f} m\")\n", + "print(f\"Tube length required = {value(m.fs.R101.length):0.6f} m\")\n", "\n", "print()\n", "print(\n", " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume, to_units=pyunits.gal)):0.3f} gal\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume, to_units=pyunits.gal)):0.6f} gal\"\n", ")\n", "\n", "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", "\n", "print()\n", "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" @@ -912,7 +912,7 @@ }, "outputs": [], "source": [ - "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, abs=1e-3)\n", + "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, rel=1e-5)\n", "assert (\n", " value(m.fs.R101.length)\n", " / value(m.fs.R101.config.finite_elements)\n", @@ -932,12 +932,12 @@ " )\n", " / 2\n", " )\n", - ") / 1e6 == pytest.approx(-3.440, abs=1e-3)\n", - "assert value(m.fs.R101.area) == pytest.approx(2.0870, abs=1e-3)\n", - "assert value(m.fs.R101.control_volume.length) == pytest.approx(4.9788, abs=1e-3)\n", - "assert value(m.fs.R101.volume * 1.2) == pytest.approx(12.469, abs=1e-3)\n", - "assert value(m.fs.eg_prod) == pytest.approx(225.415, abs=1e-3)\n", - "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.000, abs=1e-3)" + ") / 1e6 == pytest.approx(-3.789565, rel=1e-5)\n", + "assert value(m.fs.R101.area) == pytest.approx(2.930001, rel=1e-5)\n", + "assert value(m.fs.R101.control_volume.length) == pytest.approx(4.982470, rel=1e-5)\n", + "assert value(m.fs.R101.volume * 1.2) == pytest.approx(17.518369, rel=1e-5)\n", + "assert value(m.fs.eg_prod) == pytest.approx(225.415471, rel=1e-5)\n", + "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.000, rel=1e-5)" ] }, { @@ -972,9 +972,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.16" + "version": "3.9.18" } }, "nbformat": 4, - "nbformat_minor": 3 + "nbformat_minor": 4 } diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_doc.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_doc.ipynb index 3ecaff51..05620386 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_doc.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_doc.ipynb @@ -1,1340 +1,809 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "tags": [ - "header", - "hide-cell" - ] - }, - "outputs": [], - "source": [ - "###############################################################################\n", - "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", - "# Framework (IDAES IP) was produced under the DOE Institute for the\n", - "# Design of Advanced Energy Systems (IDAES).\n", - "#\n", - "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", - "# University of California, through Lawrence Berkeley National Laboratory,\n", - "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", - "# University, West Virginia University Research Corporation, et al.\n", - "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", - "# for full copyright and license information.\n", - "###############################################################################" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Flowsheet Plug Flow Reactor (PFR) Simulation and Optimization of Ethylene Glycol Production\n", - "Author: Andrew Lee \n", - "Maintainer: Andrew Lee \n", - "\n", - "\n", - "## Learning Outcomes\n", - "\n", - "\n", - "- Call and implement the IDAES PFR unit model\n", - "- Construct a steady-state flowsheet using the IDAES unit model library\n", - "- Connecting unit models in a flowsheet using Arcs\n", - "- Fomulate and solve an optimization problem\n", - " - Defining an objective function\n", - " - Setting variable bounds\n", - " - Adding additional constraints \n", - "\n", - "\n", - "## Problem Statement\n", - "\n", - "Following the previous example implementing a [Continuous Stirred Tank Reactor (CSTR) unit model](http://localhost:8888/notebooks/GitHub/examples-pse/src/Examples/UnitModels/Reactors/cstr_testing_doc.md), we can alter the flowsheet to use a plug flow reactor (PFR). As before, this example is adapted from Fogler, H.S., Elements of Chemical Reaction Engineering 5th ed., 2016, Prentice Hall, p. 157-160 with the following chemical reaction, property packages and flowsheet. Unlike a CSTR which assumes well-mixed liquid behavior, the concentration profiles will vary spatially in one dimension. In actuality, following start-up flow reactor exhibit dynamic behavior as they approach a steady-state equilibrium; we will assume our system has already achieved steady-state behavior. The state variables chosen for the property package are **molar flows of each component by phase in each stream, temperature of each stream and pressure of each stream**. The components considered are: **ethylene oxide, water, sulfuric acid and ethylene glycol** and the process occurs in liquid phase only. Therefore, every stream has 4 flow variables, 1 temperature and 1 pressure variable.\n", - "\n", - "Chemical reaction:\n", - "\n", - "**C2H4O + H2O + H2SO4 → C2H6O2 + H2SO4**\n", - "\n", - "Property Packages:\n", - "\n", - "- egprod_ideal.py\n", - "- egprod_reaction.py\n", - "\n", - "Flowsheet\n", - "\n", - "![](egprod_flowsheet.png)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Pyomo and IDAES components\n", - "\n", - "\n", - "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages. Let us first import the following components from Pyomo:\n", - "- Constraint (to write constraints)\n", - "- Var (to declare variables)\n", - "- ConcreteModel (to create the concrete model object)\n", - "- Expression (to evaluate values as a function of variables defined in the model)\n", - "- Objective (to define an objective function for optimization)\n", - "- TransformationFactory (to apply certain transformations)\n", - "- Arc (to connect two unit models)\n", - "\n", - "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\n", - "\n", - "From idaes, we will be needing the `FlowsheetBlock` and the following unit models:\n", - "- Mixer\n", - "- Heater\n", - "- PFR\n", - "\n", - "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom, tools for model expressions and calling variable values, and built-in functions to define property packages, add unit containers to objects and define our initialization scheme.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from pyomo.environ import (\n", - " Constraint,\n", - " Var,\n", - " ConcreteModel,\n", - " Expression,\n", - " Objective,\n", - " TransformationFactory,\n", - " value,\n", - " units as pyunits,\n", - ")\n", - "from pyomo.network import Arc\n", - "\n", - "from idaes.core import FlowsheetBlock\n", - "from idaes.models.properties.modular_properties import (\n", - " GenericParameterBlock,\n", - " GenericReactionParameterBlock,\n", - ")\n", - "from idaes.models.unit_models import Feed, Mixer, Heater, PFR, Product\n", - "\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.util.initialization import propagate_state" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Thermophysical and Reaction Packages\n", - "\n", - "The final step is to import the thermophysical and reaction packages. We have created a custom thermophysical package that support ideal vapor and liquid behavior for this system, and in this case we will restrict it to ideal liquid behavior only.\n", - "\n", - "The reaction package here assumes Arrhenius kinetic behavior for the PFR, for which $k_0$ and $E_a$ are known *a priori* (if unknown, they may be obtained using one of the parameter estimation tools within IDAES).\n", - "\n", - "$ r = -kVC_{EO} $, $ k = k_0 e^{(-E_a/RT)}$, with the variables as follows:\n", - "\n", - "$r$ - reaction rate extent in moles of ethylene oxide consumed per second; note that the traditional reaction rate would be given by $rate = r/V$ in moles per $m^3$ per second \n", - "$k$ - reaction rate constant per second \n", - "$V$ - volume of PFR in $m^3$, note that this is *liquid volume* and not the *total volume* of the reactor itself \n", - "$C_{EO}$ - bulk concentration of ethylene oxide in moles per $m^3$ (the limiting reagent, since we assume excess catalyst and water) \n", - "$k_0$ - pre-exponential Arrhenius factor per second \n", - "$E_a$ - reaction activation energy in kJ per mole of ethylene oxide consumed \n", - "$R$ - gas constant in J/mol-K \n", - "$T$ - reactor temperature in K\n", - "\n", - "These calculations are contained within the property, reaction and unit model packages, and do not need to be entered into the flowsheet. More information on property estimation may be found in the IDAES documentation on [Parameter Estimation](https://idaes-pse.readthedocs.io/en/stable/how_to_guides/workflow/data_rec_parmest.html).\n", - "\n", - "Let us import the following modules from the same directory as this Jupyter notebook:\n", - "- egprod_ideal as thermo_props\n", - "- egprod_reaction as reaction_props" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import egprod_ideal as thermo_props\n", - "import egprod_reaction as reaction_props" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Constructing the Flowsheet\n", - "\n", - "We have now imported all the components, unit models, and property modules we need to construct a flowsheet. Let us create a ConcreteModel and add the flowsheet block. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "m = ConcreteModel()\n", - "m.fs = FlowsheetBlock(dynamic=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now need to add the property packages to the flowsheet. Unlike the basic [Flash unit model example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Tutorials/Basics/flash_unit_solution_testing_doc.md), where we only had a thermophysical property package, for this flowsheet we will also need to add a reaction property package. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) and [Modular Reaction Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-reaction-package-framework). The get_prop method for the natural gas property module automatically returns the correct dictionary using a component list argument. The GenericParameterBlock and GenericReactionParameterBlock methods build states blocks from passed parameter data; the reaction block unpacks using **reaction_props.config_dict to allow for optional or empty keyword arguments:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.fs.thermo_params = GenericParameterBlock(**thermo_props.config_dict)\n", - "m.fs.reaction_params = GenericReactionParameterBlock(\n", - " property_package=m.fs.thermo_params, **reaction_props.config_dict\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Unit Models\n", - "\n", - "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding a `Mixer`, a `Heater` and a `PFR`. Note that all unit models need to be given a property package argument. In addition to that, there are several arguments depending on the unit model, please refer to the documentation for more details on [IDAES Unit Models](https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/index.html). For example, the `Mixer` is given a `list` consisting of names to the two inlets." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.fs.OXIDE = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.ACID = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", - "m.fs.M101 = Mixer(\n", - " property_package=m.fs.thermo_params, inlet_list=[\"reagent_feed\", \"catalyst_feed\"]\n", - ")\n", - "m.fs.H101 = Heater(\n", - " property_package=m.fs.thermo_params,\n", - " has_pressure_change=False,\n", - " has_phase_equilibrium=False,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "m.fs.R101 = PFR(\n", - " property_package=m.fs.thermo_params,\n", - " reaction_package=m.fs.reaction_params,\n", - " has_equilibrium_reactions=False,\n", - " has_heat_of_reaction=True,\n", - " has_heat_transfer=True,\n", - " has_pressure_change=False,\n", - " transformation_method=\"dae.finite_difference\",\n", - " transformation_scheme=\"BACKWARD\",\n", - " finite_elements=20,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Connecting Unit Models Using Arcs\n", - "\n", - "We have now added all the unit models we need to the flowsheet. However, we have not yet specified how the units are to be connected. To do this, we will be using the `Arc` which is a pyomo component that takes in two arguments: `source` and `destination`. Let us connect the outlet of the `Mixer` to the inlet of the `Heater`, and the outlet of the `Heater` to the inlet of the `PFR`. Additionally, we will connect the `Feed` and `Product` blocks to the flowsheet:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.s01 = Arc(source=m.fs.OXIDE.outlet, destination=m.fs.M101.reagent_feed)\n", - "m.fs.s02 = Arc(source=m.fs.ACID.outlet, destination=m.fs.M101.catalyst_feed)\n", - "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)\n", - "m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", - "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We have now connected the unit model block using the arcs. However, we also need to link the state variables on connected ports. Pyomo provides a convenient method `TransformationFactory` to write these equality constraints for us between two ports:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "TransformationFactory(\"network.expand_arcs\").apply_to(m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Expressions to Compute Operating Costs\n", - "\n", - "In this section, we will add a few Expressions that allows us to evaluate the performance. `Expressions` provide a convenient way of calculating certain values that are a function of the variables defined in the model. For more details on `Expressions`, please refer to the [Pyomo Expression documentation]( https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html).\n", - "\n", - "For this flowsheet, we are interested in computing ethylene glycol production in millions of pounds per year, as well as the total costs due to cooling and heating utilities." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us first add an `Expression` to convert the product flow from mol/s to MM lb/year of ethylene glycol. We see that the molecular weight exists in the thermophysical property package, so we may use that value for our calculations." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.eg_prod = Expression(\n", - " expr=pyunits.convert(\n", - " m.fs.PROD.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - " * m.fs.thermo_params.ethylene_glycol.mw, # MW defined in properties as kg/mol\n", - " to_units=pyunits.Mlb / pyunits.yr,\n", - " )\n", - ") # converting kg/s to MM lb/year" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, let us add expressions to compute the reactor cooling cost (\\\\$/s) assuming a cost of 2.12E-5 \\\\$/kW, and the heating utility cost (\\\\$/s) assuming 2.2E-4 \\\\$/kW. To calculate cooling cost, it is important to note that the heat duty is not constant throughout the reactor's length and is expressed in terms of heat per length (J/m/s). This is why we utilize the trapezoid rule to calculate the total heat duty of the reactor:$Q=\\Delta x\\big(\\sum_{k=1}^{N-1}(Q_k)+\\frac{Q_N+Q_0}{2}\\big)$ \n", - "where k is the subinterval in the length domain, N is the number of intervals, and $\\Delta x$ is the length of the interval.\n", - "Note that the heat duty is in units of watt (J/s). The total operating cost will be the sum of the two, expressed in \\\\$/year, assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.fs.cooling_cost = Expression(\n", - " expr=2.12e-8\n", - " * m.fs.R101.length\n", - " / m.fs.R101.config.finite_elements\n", - " * (\n", - " -sum(\n", - " m.fs.R101.heat_duty[0, k]\n", - " for k in m.fs.R101.control_volume.length_domain\n", - " if 0.0 <= k < 1.0\n", - " )\n", - " )\n", - " - (\n", - " value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", - " - value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)])\n", - " )\n", - " / 2\n", - ") # the reaction is exothermic, so R101 duty is negative\n", - "m.fs.heating_cost = Expression(\n", - " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", - ") # the stream must be heated to T_rxn, so H101 duty is positive\n", - "m.fs.operating_cost = Expression(\n", - " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost))\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Feed Conditions\n", - "\n", - "Let us first check how many degrees of freedom exist for this flowsheet using the `degrees_of_freedom` tool we imported earlier. We expect each stream to have 6 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 2 unit specifications and 1 specification for each finite element. Therefore, we have 35 degrees of freedom to specify: temperature, pressure and flow of all four components on both streams; outlet heater temperature; a reactor property such as conversion or heat duty at each finite element; reactor volume and reactor length." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "scrolled": true - }, - "outputs": [ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [ + "header", + "hide-cell" + ] + }, + "outputs": [], + "source": [ + "###############################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "###############################################################################" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "35\n" - ] - } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will now be fixing the feed stream to the conditions shown in the flowsheet above. As mentioned in other tutorials, the IDAES framework expects a time index value for every referenced internal stream or unit variable, even in steady-state systems with a single time point $ t = 0 $ (`t = [0]` is the default when creating a `FlowsheetBlock` without passing a `time_set` argument). The non-present components in each stream are assigned a very small non-zero value to help with convergence and initializing. Based on stoichiometric ratios for the reaction, 80% conversion and 200 MM lb/year (46.4 mol/s) of ethylene glycol, we will initialize our simulation with the following calculated values:" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", - " 58.0 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", - " 39.6 * pyunits.mol / pyunits.s\n", - ") # calculated from 16.1 mol EO / cudm in stream\n", - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.OXIDE.outlet.temperature.fix(298.15 * pyunits.K)\n", - "m.fs.OXIDE.outlet.pressure.fix(1e5 * pyunits.Pa)\n", - "\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", - " 200 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", - " 0.334 * pyunits.mol / pyunits.s\n", - ") # calculated from 0.9 wt% SA in stream\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.ACID.outlet.temperature.fix(298.15 * pyunits.K)\n", - "m.fs.ACID.outlet.pressure.fix(1e5 * pyunits.Pa)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Unit Model Specifications\n", - "\n", - "Now that we have fixed our inlet feed conditions, we will now be fixing the operating conditions for the unit models in the flowsheet. Let us fix the outlet temperature of H101 to 328.15 K. " - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.H101.outlet.temperature.fix(328.15 * pyunits.K)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For the `PFR`, we have to define the conversion in terms of ethylene oxide. Note that the `PFR` reaction volume variable (m.fs.R101.volume) does not need to be defined here since it is internally defined by the `PFR` model. We'll estimate 50% conversion for our initial flowsheet:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "m.fs.R101.conversion = Var(\n", - " bounds=(0, 1), initialize=0.80, units=pyunits.dimensionless\n", - ") # fraction\n", - "\n", - "m.fs.R101.conv_constraint = Constraint(\n", - " expr=m.fs.R101.conversion\n", - " * m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", - " == (\n", - " m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", - " - m.fs.R101.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", - " )\n", - ")\n", - "\n", - "for x in m.fs.R101.control_volume.length_domain:\n", - " if x == 0:\n", - " continue\n", - " m.fs.R101.control_volume.properties[0, x].temperature.fix(\n", - " 328.15 * pyunits.K\n", - " ) # equal inlet reactor temperature\n", - "\n", - "m.fs.R101.conversion.fix(0.5)\n", - "\n", - "m.fs.R101.length.fix(1 * pyunits.m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As we did not place a specification on reactor duty, the solver may try positive values to increase the reaction temperature and rate. To prevent the optimization from diverging, we need to set an upper bound restricting heat flow to cooling only:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.R101.heat_duty.setub(\n", - " 0 * pyunits.J / pyunits.m / pyunits.s\n", - ") # heat duty is only used for cooling" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "# Flowsheet Plug Flow Reactor (PFR) Simulation and Optimization of Ethylene Glycol Production\n", + "Author: Andrew Lee \n", + "Maintainer: Andrew Lee \n", + "\n", + "\n", + "## Learning Outcomes\n", + "\n", + "\n", + "- Call and implement the IDAES PFR unit model\n", + "- Construct a steady-state flowsheet using the IDAES unit model library\n", + "- Connecting unit models in a flowsheet using Arcs\n", + "- Fomulate and solve an optimization problem\n", + " - Defining an objective function\n", + " - Setting variable bounds\n", + " - Adding additional constraints \n", + "\n", + "\n", + "## Problem Statement\n", + "\n", + "Following the previous example implementing a [Continuous Stirred Tank Reactor (CSTR) unit model](http://localhost:8888/notebooks/GitHub/examples-pse/src/Examples/UnitModels/Reactors/cstr_testing_doc.md), we can alter the flowsheet to use a plug flow reactor (PFR). As before, this example is adapted from Fogler, H.S., Elements of Chemical Reaction Engineering 5th ed., 2016, Prentice Hall, p. 157-160 with the following chemical reaction, property packages and flowsheet. Unlike a CSTR which assumes well-mixed liquid behavior, the concentration profiles will vary spatially in one dimension. In actuality, following start-up flow reactor exhibit dynamic behavior as they approach a steady-state equilibrium; we will assume our system has already achieved steady-state behavior. The state variables chosen for the property package are **molar flows of each component by phase in each stream, temperature of each stream and pressure of each stream**. The components considered are: **ethylene oxide, water, sulfuric acid and ethylene glycol** and the process occurs in liquid phase only. Therefore, every stream has 4 flow variables, 1 temperature and 1 pressure variable.\n", + "\n", + "Chemical reaction:\n", + "\n", + "**C2H4O + H2O + H2SO4 \u2192 C2H6O2 + H2SO4**\n", + "\n", + "Property Packages:\n", + "\n", + "- egprod_ideal.py\n", + "- egprod_reaction.py\n", + "\n", + "Flowsheet\n", + "\n", + "![](egprod_flowsheet.png)" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "0\n" - ] - } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we need to initialize the each unit operation in sequence to solve the flowsheet. As in best practice, unit operations are initialized or solved, and outlet properties are propagated to connected inlet streams via arc definitions as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Pyomo and IDAES components\n", + "\n", + "\n", + "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages. Let us first import the following components from Pyomo:\n", + "- Constraint (to write constraints)\n", + "- Var (to declare variables)\n", + "- ConcreteModel (to create the concrete model object)\n", + "- Expression (to evaluate values as a function of variables defined in the model)\n", + "- Objective (to define an objective function for optimization)\n", + "- TransformationFactory (to apply certain transformations)\n", + "- Arc (to connect two unit models)\n", + "\n", + "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\n", + "\n", + "From idaes, we will be needing the `FlowsheetBlock` and the following unit models:\n", + "- Mixer\n", + "- Heater\n", + "- PFR\n", + "\n", + "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom, tools for model expressions and calling variable values, and built-in functions to define property packages, add unit containers to objects and define our initialization scheme.\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.OXIDE.properties: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "from pyomo.environ import (\n", + " Constraint,\n", + " Var,\n", + " ConcreteModel,\n", + " Expression,\n", + " Objective,\n", + " TransformationFactory,\n", + " value,\n", + " units as pyunits,\n", + ")\n", + "from pyomo.network import Arc\n", + "\n", + "from idaes.core import FlowsheetBlock\n", + "from idaes.models.properties.modular_properties import (\n", + " GenericParameterBlock,\n", + " GenericReactionParameterBlock,\n", + ")\n", + "from idaes.models.unit_models import Feed, Mixer, Heater, PFR, Product\n", + "\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.util.initialization import propagate_state" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.OXIDE.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Thermophysical and Reaction Packages\n", + "\n", + "The final step is to import the thermophysical and reaction packages. We have created a custom thermophysical package that support ideal vapor and liquid behavior for this system, and in this case we will restrict it to ideal liquid behavior only.\n", + "\n", + "The reaction package here assumes Arrhenius kinetic behavior for the PFR, for which $k_0$ and $E_a$ are known *a priori* (if unknown, they may be obtained using one of the parameter estimation tools within IDAES).\n", + "\n", + "$ r = -kVC_{EO} $, $ k = k_0 e^{(-E_a/RT)}$, with the variables as follows:\n", + "\n", + "$r$ - reaction rate extent in moles of ethylene oxide consumed per second; note that the traditional reaction rate would be given by $rate = r/V$ in moles per $m^3$ per second \n", + "$k$ - reaction rate constant per second \n", + "$V$ - volume of PFR in $m^3$, note that this is *liquid volume* and not the *total volume* of the reactor itself \n", + "$C_{EO}$ - bulk concentration of ethylene oxide in moles per $m^3$ (the limiting reagent, since we assume excess catalyst and water) \n", + "$k_0$ - pre-exponential Arrhenius factor per second \n", + "$E_a$ - reaction activation energy in kJ per mole of ethylene oxide consumed \n", + "$R$ - gas constant in J/mol-K \n", + "$T$ - reactor temperature in K\n", + "\n", + "These calculations are contained within the property, reaction and unit model packages, and do not need to be entered into the flowsheet. More information on property estimation may be found in the IDAES documentation on [Parameter Estimation](https://idaes-pse.readthedocs.io/en/stable/how_to_guides/workflow/data_rec_parmest.html).\n", + "\n", + "Let us import the following modules from the same directory as this Jupyter notebook:\n", + "- egprod_ideal as thermo_props\n", + "- egprod_reaction as reaction_props" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.OXIDE.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import egprod_ideal as thermo_props\n", + "import egprod_reaction as reaction_props" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.OXIDE: Initialization Complete.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constructing the Flowsheet\n", + "\n", + "We have now imported all the components, unit models, and property modules we need to construct a flowsheet. Let us create a ConcreteModel and add the flowsheet block. " + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.ACID.properties: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "m = ConcreteModel()\n", + "m.fs = FlowsheetBlock(dynamic=False)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.ACID.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to add the property packages to the flowsheet. Unlike the basic [Flash unit model example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Tutorials/Basics/flash_unit_solution_testing_doc.md), where we only had a thermophysical property package, for this flowsheet we will also need to add a reaction property package. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) and [Modular Reaction Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-reaction-package-framework). The get_prop method for the natural gas property module automatically returns the correct dictionary using a component list argument. The GenericParameterBlock and GenericReactionParameterBlock methods build states blocks from passed parameter data; the reaction block unpacks using **reaction_props.config_dict to allow for optional or empty keyword arguments:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.ACID.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 5, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "m.fs.thermo_params = GenericParameterBlock(**thermo_props.config_dict)\n", + "m.fs.reaction_params = GenericReactionParameterBlock(\n", + " property_package=m.fs.thermo_params, **reaction_props.config_dict\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.ACID: Initialization Complete.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Unit Models\n", + "\n", + "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding a `Mixer`, a `Heater` and a `PFR`. Note that all unit models need to be given a property package argument. In addition to that, there are several arguments depending on the unit model, please refer to the documentation for more details on [IDAES Unit Models](https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/index.html). For example, the `Mixer` is given a `list` consisting of names to the two inlets." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.M101.reagent_feed_state: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "m.fs.OXIDE = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.ACID = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", + "m.fs.M101 = Mixer(\n", + " property_package=m.fs.thermo_params, inlet_list=[\"reagent_feed\", \"catalyst_feed\"]\n", + ")\n", + "m.fs.H101 = Heater(\n", + " property_package=m.fs.thermo_params,\n", + " has_pressure_change=False,\n", + " has_phase_equilibrium=False,\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.M101.reagent_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 7, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "m.fs.R101 = PFR(\n", + " property_package=m.fs.thermo_params,\n", + " reaction_package=m.fs.reaction_params,\n", + " has_equilibrium_reactions=False,\n", + " has_heat_of_reaction=True,\n", + " has_heat_transfer=True,\n", + " has_pressure_change=False,\n", + " transformation_method=\"dae.finite_difference\",\n", + " transformation_scheme=\"BACKWARD\",\n", + " finite_elements=20,\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.M101.catalyst_feed_state: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Connecting Unit Models Using Arcs\n", + "\n", + "We have now added all the unit models we need to the flowsheet. However, we have not yet specified how the units are to be connected. To do this, we will be using the `Arc` which is a pyomo component that takes in two arguments: `source` and `destination`. Let us connect the outlet of the `Mixer` to the inlet of the `Heater`, and the outlet of the `Heater` to the inlet of the `PFR`. Additionally, we will connect the `Feed` and `Product` blocks to the flowsheet:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.M101.catalyst_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.s01 = Arc(source=m.fs.OXIDE.outlet, destination=m.fs.M101.reagent_feed)\n", + "m.fs.s02 = Arc(source=m.fs.ACID.outlet, destination=m.fs.M101.catalyst_feed)\n", + "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)\n", + "m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", + "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have now connected the unit model block using the arcs. However, we also need to link the state variables on connected ports. Pyomo provides a convenient method `TransformationFactory` to write these equality constraints for us between two ports:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "TransformationFactory(\"network.expand_arcs\").apply_to(m)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Expressions to Compute Operating Costs\n", + "\n", + "In this section, we will add a few Expressions that allows us to evaluate the performance. `Expressions` provide a convenient way of calculating certain values that are a function of the variables defined in the model. For more details on `Expressions`, please refer to the [Pyomo Expression documentation]( https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html).\n", + "\n", + "For this flowsheet, we are interested in computing ethylene glycol production in millions of pounds per year, as well as the total costs due to cooling and heating utilities." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us first add an `Expression` to convert the product flow from mol/s to MM lb/year of ethylene glycol. We see that the molecular weight exists in the thermophysical property package, so we may use that value for our calculations." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.eg_prod = Expression(\n", + " expr=pyunits.convert(\n", + " m.fs.PROD.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", + " * m.fs.thermo_params.ethylene_glycol.mw, # MW defined in properties as kg/mol\n", + " to_units=pyunits.Mlb / pyunits.yr,\n", + " )\n", + ") # converting kg/s to MM lb/year" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let us add expressions to compute the reactor cooling cost (\\\\$/s) assuming a cost of 2.12E-5 \\\\$/kW, and the heating utility cost (\\\\$/s) assuming 2.2E-4 \\\\$/kW. To calculate cooling cost, it is important to note that the heat duty is not constant throughout the reactor's length and is expressed in terms of heat per length (J/m/s). This is why we utilize the trapezoid rule to calculate the total heat duty of the reactor:$Q=\\Delta x\\big(\\sum_{k=1}^{N-1}(Q_k)+\\frac{Q_N+Q_0}{2}\\big)$ \n", + "where k is the subinterval in the length domain, N is the number of intervals, and $\\Delta x$ is the length of the interval.\n", + "Note that the heat duty is in units of watt (J/s). The total operating cost will be the sum of the two, expressed in \\\\$/year, assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 11, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "m.fs.cooling_cost = Expression(\n", + " expr=2.12e-8\n", + " * m.fs.R101.length\n", + " / m.fs.R101.config.finite_elements\n", + " * (\n", + " -sum(\n", + " m.fs.R101.heat_duty[0, k]\n", + " for k in m.fs.R101.control_volume.length_domain\n", + " if 0.0 <= k < 1.0\n", + " )\n", + " )\n", + " - (\n", + " value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", + " - value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)])\n", + " )\n", + " / 2\n", + ") # the reaction is exothermic, so R101 duty is negative\n", + "m.fs.heating_cost = Expression(\n", + " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", + ") # the stream must be heated to T_rxn, so H101 duty is positive\n", + "m.fs.operating_cost = Expression(\n", + " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost))\n", + ")" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Feed Conditions\n", + "\n", + "Let us first check how many degrees of freedom exist for this flowsheet using the `degrees_of_freedom` tool we imported earlier. We expect each stream to have 6 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 2 unit specifications and 1 specification for each finite element. Therefore, we have 35 degrees of freedom to specify: temperature, pressure and flow of all four components on both streams; outlet heater temperature; a reactor property such as conversion or heat duty at each finite element; reactor volume and reactor length." + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n" - ] + "cell_type": "code", + "execution_count": 12, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "print(degrees_of_freedom(m))" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now be fixing the feed stream to the conditions shown in the flowsheet above. As mentioned in other tutorials, the IDAES framework expects a time index value for every referenced internal stream or unit variable, even in steady-state systems with a single time point $ t = 0 $ (`t = [0]` is the default when creating a `FlowsheetBlock` without passing a `time_set` argument). The non-present components in each stream are assigned a very small non-zero value to help with convergence and initializing. Based on stoichiometric ratios for the reaction, 80% conversion and 200 MM lb/year (46.4 mol/s) of ethylene glycol, we will initialize our simulation with the following calculated values:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.R101.control_volume.properties: Starting initialization\n" - ] + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", + " 58.0 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", + " 39.6 * pyunits.mol / pyunits.s\n", + ") # calculated from 16.1 mol EO / cudm in stream\n", + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.OXIDE.outlet.temperature.fix(298.15 * pyunits.K)\n", + "m.fs.OXIDE.outlet.pressure.fix(1e5 * pyunits.Pa)\n", + "\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", + " 200 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", + " 0.334 * pyunits.mol / pyunits.s\n", + ") # calculated from 0.9 wt% SA in stream\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.ACID.outlet.temperature.fix(298.15 * pyunits.K)\n", + "m.fs.ACID.outlet.pressure.fix(1e5 * pyunits.Pa)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.R101.control_volume.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Unit Model Specifications\n", + "\n", + "Now that we have fixed our inlet feed conditions, we will now be fixing the operating conditions for the unit models in the flowsheet. Let us fix the outlet temperature of H101 to 328.15 K. " + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.R101.control_volume.reactions: Initialization Complete.\n" - ] + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.H101.outlet.temperature.fix(328.15 * pyunits.K)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the `PFR`, we have to define the conversion in terms of ethylene oxide. Note that the `PFR` reaction volume variable (m.fs.R101.volume) does not need to be defined here since it is internally defined by the `PFR` model. We'll estimate 50% conversion for our initial flowsheet:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n" - ] + "cell_type": "code", + "execution_count": 16, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "m.fs.R101.conversion = Var(\n", + " bounds=(0, 1), initialize=0.80, units=pyunits.dimensionless\n", + ") # fraction\n", + "\n", + "m.fs.R101.conv_constraint = Constraint(\n", + " expr=m.fs.R101.conversion\n", + " * m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", + " == (\n", + " m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", + " - m.fs.R101.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", + " )\n", + ")\n", + "\n", + "for x in m.fs.R101.control_volume.length_domain:\n", + " if x == 0:\n", + " continue\n", + " m.fs.R101.control_volume.properties[0, x].temperature.fix(\n", + " 328.15 * pyunits.K\n", + " ) # equal inlet reactor temperature\n", + "\n", + "m.fs.R101.conversion.fix(0.5)\n", + "\n", + "m.fs.R101.length.fix(1 * pyunits.m)" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As we did not place a specification on reactor duty, the solver may try positive values to increase the reaction temperature and rate. To prevent the optimization from diverging, we need to set an upper bound restricting heat flow to cooling only:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.heat_duty.setub(\n", + " 0 * pyunits.J / pyunits.m / pyunits.s\n", + ") # heat duty is only used for cooling" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:21:57 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" - ] - } - ], - "source": [ - "# Initialize and solve each unit operation\n", - "m.fs.OXIDE.initialize()\n", - "propagate_state(arc=m.fs.s01)\n", - "\n", - "m.fs.ACID.initialize()\n", - "propagate_state(arc=m.fs.s01)\n", - "\n", - "m.fs.M101.initialize()\n", - "propagate_state(arc=m.fs.s03)\n", - "\n", - "m.fs.H101.initialize()\n", - "propagate_state(arc=m.fs.s04)\n", - "\n", - "m.fs.R101.initialize()\n", - "propagate_state(arc=m.fs.s05)\n", - "\n", - "m.fs.PROD.initialize()\n", - "\n", - "# set solver\n", - "solver = get_solver()" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": { - "scrolled": true - }, - "outputs": [ + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "print(degrees_of_freedom(m))" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 1923\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 1323\n", - "\n", - "Total number of variables............................: 608\n", - " variables with only lower bounds: 0\n", - " variables with lower and upper bounds: 257\n", - " variables with only upper bounds: 20\n", - "Total number of equality constraints.................: 608\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.30e+06 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 1.67e+07 5.52e+03 -1.0 1.17e+08 - 2.85e-01 9.90e-01f 1\n", - " 2 0.0000000e+00 2.61e+05 6.25e+03 -1.0 1.17e+06 - 8.25e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 2.59e+03 3.50e+01 -1.0 1.17e+04 - 9.90e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 1.97e+01 3.22e+03 -1.0 1.15e+02 - 9.90e-01 9.92e-01h 1\n", - " 5 0.0000000e+00 3.19e-07 4.81e+03 -1.0 8.77e-01 - 9.91e-01 1.00e+00h 1\n", - "Cannot recompute multipliers for feasibility problem. Error in eq_mult_calculator\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 1.6686898115291998e+06 1.6686898115291998e+06\n", - "Constraint violation....: 3.1874515116214752e-07 3.1874515116214752e-07\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 3.1874515116214752e-07 1.6686898115291998e+06\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.003\n", - "Total CPU secs in NLP function evaluations = 0.003\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "# Solve the model\n", - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Analyze the Results of the Square Problem\n", - "\n", - "\n", - "What is the total operating cost? " - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we need to initialize the each unit operation in sequence to solve the flowsheet. As in best practice, unit operations are initialized or solved, and outlet properties are propagated to connected inlet streams via arc definitions as follows:" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $2.082 million per year\n" - ] - } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For this operating cost, what conversion did we achieve of ethylene oxide to ethylene glycol? " - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize and solve each unit operation\n", + "m.fs.OXIDE.initialize()\n", + "propagate_state(arc=m.fs.s01)\n", + "\n", + "m.fs.ACID.initialize()\n", + "propagate_state(arc=m.fs.s01)\n", + "\n", + "m.fs.M101.initialize()\n", + "propagate_state(arc=m.fs.s03)\n", + "\n", + "m.fs.H101.initialize()\n", + "propagate_state(arc=m.fs.s04)\n", + "\n", + "m.fs.R101.initialize()\n", + "propagate_state(arc=m.fs.s05)\n", + "\n", + "m.fs.PROD.initialize()\n", + "\n", + "# set solver\n", + "solver = get_solver()" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "====================================================================================\n", - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Area : 1.1490 : meter ** 2 : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Molar Flowrate ('Liq', 'ethylene_oxide') mole / second 58.000 29.000\n", - " Molar Flowrate ('Liq', 'water') mole / second 239.60 210.60\n", - " Molar Flowrate ('Liq', 'sulfuric_acid') mole / second 0.33401 0.33401\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 2.0000e-05 29.000\n", - " Temperature kelvin 328.15 328.15\n", - " Pressure pascal 1.0000e+05 1.0000e+05\n", - "====================================================================================\n", - "\n", - "Conversion achieved = 50.0%\n", - "\n", - "Total heat duty required = -3.469 MJ\n", - "\n", - "Tube area required = 1.149 m^2\n", - "\n", - "Tube length required = 1.000 m\n", - "\n", - "Tube volume required = 1.149 m^3\n" - ] - } - ], - "source": [ - "m.fs.R101.report()\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")\n", - "print()\n", - "print(\n", - " f\"Total heat duty required = \"\n", - " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * \n", - " (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", - " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", - " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.3f}\"\"\"\n", - " f\" MJ\"\n", - ")\n", - "print()\n", - "print(f\"Tube area required = {value(m.fs.R101.area):0.3f} m^2\")\n", - "print()\n", - "print(f\"Tube length required = {value(m.fs.R101.length):0.3f} m\")\n", - "print()\n", - "print(f\"Tube volume required = {value(m.fs.R101.volume):0.3f} m^3\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Optimizing Ethylene Glycol Production\n", - "\n", - "Now that the flowsheet has been squared and solved, we can run a small optimization problem to minimize our production costs. Suppose we require at least 200 million pounds/year of ethylene glycol produced and 90% conversion of ethylene oxide, allowing for variable reactor volume (considering operating/non-capital costs only) and reactor temperature (heater outlet)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us declare our objective function for this problem. " - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.objective = Objective(expr=m.fs.operating_cost)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now, as well as set bounds for the design variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.eg_prod_con = Constraint(\n", - " expr=m.fs.eg_prod >= 200 * pyunits.Mlb / pyunits.yr\n", - ") # MM lb/year\n", - "m.fs.R101.conversion.fix(0.90)\n", - "\n", - "m.fs.R101.volume.setlb(0 * pyunits.m**3)\n", - "m.fs.R101.volume.setub(pyunits.convert(5000 * pyunits.gal, to_units=pyunits.m**3))\n", - "\n", - "m.fs.R101.length.unfix()\n", - "m.fs.R101.length.setlb(0 * pyunits.m)\n", - "m.fs.R101.length.setub(5 * pyunits.m)\n", - "\n", - "m.fs.H101.outlet.temperature.unfix()\n", - "m.fs.H101.outlet.temperature[0].setlb(328.15 * pyunits.K)\n", - "m.fs.H101.outlet.temperature[0].setub(\n", - " 470.45 * pyunits.K\n", - ") # highest component boiling point (ethylene glycol)\n", - "\n", - "for x in m.fs.R101.control_volume.length_domain:\n", - " if x == 0:\n", - " continue\n", - " m.fs.R101.control_volume.properties[\n", - " 0, x\n", - " ].temperature.unfix() # allow for temperature change in each finite element" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "We have now defined the optimization problem and we are now ready to solve this problem. \n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "scrolled": true - }, - "outputs": [ + "cell_type": "code", + "execution_count": 21, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Solve the model\n", + "results = solver.solve(m, tee=True)" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyze the Results of the Square Problem\n", + "\n", + "\n", + "What is the total operating cost? " + ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 2067\n", - "Number of nonzeros in inequality constraint Jacobian.: 1\n", - "Number of nonzeros in Lagrangian Hessian.............: 1886\n", - "\n", - "Total number of variables............................: 631\n", - " variables with only lower bounds: 0\n", - " variables with lower and upper bounds: 280\n", - " variables with only upper bounds: 21\n", - "Total number of equality constraints.................: 608\n", - "Total number of inequality constraints...............: 1\n", - " inequality constraints with only lower bounds: 1\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 2.0817113e+06 3.66e+06 1.00e+02 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 3.7272642e+06 5.24e+05 9.25e+01 -1.0 2.16e+06 - 7.82e-02 9.90e-01h 1\n", - " 2 3.7439283e+06 5.50e+03 4.53e+02 -1.0 2.14e+04 - 7.22e-01 9.91e-01h 1\n", - " 3 3.7441018e+06 2.31e-01 7.45e+03 -1.0 3.16e+02 - 9.25e-01 1.00e+00h 1\n", - " 4 3.7445196e+06 7.21e-02 1.59e+04 -1.0 3.84e+02 - 9.90e-01 1.00e+00f 1\n", - " 5 3.7434412e+06 6.03e-01 2.88e+02 -1.0 3.91e+04 - 9.82e-01 1.00e+00F 1\n", - " 6 3.4569523e+06 1.90e+05 1.52e+02 -1.0 3.75e+06 - 4.73e-01 1.00e+00F 1\n", - " 7 1.3257122e+06 6.54e+06 1.34e+02 -1.0 7.17e+07 - 1.17e-01 4.66e-01F 1\n", - " 8 5.4503925e+05 4.55e+06 4.94e+01 -1.0 4.27e+07 - 6.32e-01 3.47e-01f 1\n", - " 9 2.2925409e+05 1.16e+06 2.53e+01 -1.0 1.78e+07 - 8.45e-01 6.02e-01h 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 2.1904785e+05 1.55e+06 3.36e+01 -1.0 1.16e+07 - 1.00e+00 1.76e-01h 1\n", - " 11 2.4068883e+05 7.81e+05 4.18e+01 -1.0 4.64e+06 - 5.75e-01 5.16e-01h 1\n", - " 12 3.6492667e+05 3.58e+03 3.24e+01 -1.0 2.13e+05 - 5.89e-01 1.00e+00h 1\n", - " 13 3.3687811e+05 1.24e+03 5.19e+05 -1.7 8.40e+05 - 1.00e+00 6.11e-01h 1\n", - " 14 3.2827810e+05 6.28e+02 3.94e-02 -1.7 1.36e+05 - 1.00e+00 1.00e+00h 1\n", - " 15 3.2852907e+05 1.59e+00 1.32e-03 -1.7 5.68e+03 - 1.00e+00 1.00e+00h 1\n", - " 16 3.1973165e+05 1.33e+01 5.15e+04 -3.8 1.94e+05 - 9.86e-01 8.31e-01f 1\n", - " 17 3.1849743e+05 2.37e-01 1.46e-03 -3.8 2.16e+04 - 1.00e+00 1.00e+00h 1\n", - " 18 3.1850187e+05 5.49e-03 1.29e-07 -3.8 1.93e+02 - 1.00e+00 1.00e+00h 1\n", - " 19 3.1843083e+05 8.45e-04 3.52e-06 -5.7 1.29e+03 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 20 3.1843086e+05 4.64e-07 2.07e-11 -5.7 1.29e+00 - 1.00e+00 1.00e+00h 1\n", - " 21 3.1842998e+05 3.55e-07 5.40e-10 -8.6 1.59e+01 - 1.00e+00 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 21\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 1.5329173454170544e+01 3.1842997505757213e+05\n", - "Dual infeasibility......: 5.3983226027300468e-10 1.1213831827784419e-05\n", - "Constraint violation....: 3.5460107028484344e-07 3.5460107028484344e-07\n", - "Complementarity.........: 2.5137892688647162e-09 5.2218461522255445e-05\n", - "Overall NLP error.......: 3.5460107028484344e-07 5.2218461522255445e-05\n", - "\n", - "\n", - "Number of objective function evaluations = 26\n", - "Number of objective gradient evaluations = 22\n", - "Number of equality constraint evaluations = 26\n", - "Number of inequality constraint evaluations = 26\n", - "Number of equality constraint Jacobian evaluations = 22\n", - "Number of inequality constraint Jacobian evaluations = 22\n", - "Number of Lagrangian Hessian evaluations = 21\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.028\n", - "Total CPU secs in NLP function evaluations = 0.005\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $0.318 million per year\n", - "\n", - "Heater results\n", - "\n", - "====================================================================================\n", - "Unit : fs.H101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : 699.26 : watt : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Molar Flowrate ('Liq', 'ethylene_oxide') mole / second 58.000 58.000\n", - " Molar Flowrate ('Liq', 'water') mole / second 239.60 239.60\n", - " Molar Flowrate ('Liq', 'sulfuric_acid') mole / second 0.33401 0.33401\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 2.0000e-05 2.0000e-05\n", - " Temperature kelvin 298.15 328.15\n", - " Pressure pascal 1.0000e+05 1.0000e+05\n", - "====================================================================================\n", - "\n", - "PFR reactor results\n", - "\n", - "====================================================================================\n", - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Area : 2.0871 : meter ** 2 : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Molar Flowrate ('Liq', 'ethylene_oxide') mole / second 58.000 5.8000\n", - " Molar Flowrate ('Liq', 'water') mole / second 239.60 187.40\n", - " Molar Flowrate ('Liq', 'sulfuric_acid') mole / second 0.33401 0.33401\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 2.0000e-05 52.200\n", - " Temperature kelvin 328.15 273.15\n", - " Pressure pascal 1.0000e+05 1.0000e+05\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", - "\n", - "print()\n", - "print(\"Heater results\")\n", - "\n", - "m.fs.H101.report()\n", - "\n", - "print()\n", - "print(\"PFR reactor results\")\n", - "\n", - "m.fs.R101.report()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Display optimal values for the decision variables and design variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this operating cost, what conversion did we achieve of ethylene oxide to ethylene glycol? " + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.report()\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")\n", + "print()\n", + "print(\n", + " f\"Total heat duty required = \"\n", + " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * \n", + " (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", + " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", + " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.6f}\"\"\"\n", + " f\" MJ\"\n", + ")\n", + "print()\n", + "print(f\"Tube area required = {value(m.fs.R101.area):0.6f} m^2\")\n", + "print()\n", + "print(f\"Tube length required = {value(m.fs.R101.length):0.6f} m\")\n", + "print()\n", + "print(f\"Tube volume required = {value(m.fs.R101.volume):0.6f} m^3\")" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimal Values\n", - "\n", - "H101 outlet temperature = 328.150 K\n", - "\n", - "Total heat duty required = -3.440 MJ\n", - "\n", - "Tube area required = 2.087 m^2\n", - "\n", - "Tube length required = 4.979 m\n", - "\n", - "Assuming a 20% design factor for reactor volume,total CSTR volume required = 12.469 m^3 = 3294.093 gal\n", - "\n", - "Ethylene glycol produced = 225.415 MM lb/year\n", - "\n", - "Conversion achieved = 90.0%\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimizing Ethylene Glycol Production\n", + "\n", + "Now that the flowsheet has been squared and solved, we can run a small optimization problem to minimize our production costs. Suppose we require at least 200 million pounds/year of ethylene glycol produced and 90% conversion of ethylene oxide, allowing for variable reactor volume (considering operating/non-capital costs only) and reactor temperature (heater outlet)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us declare our objective function for this problem. " + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.objective = Objective(expr=m.fs.operating_cost)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now, as well as set bounds for the design variables:" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.eg_prod_con = Constraint(\n", + " expr=m.fs.eg_prod >= 200 * pyunits.Mlb / pyunits.yr\n", + ") # MM lb/year\n", + "m.fs.R101.conversion.fix(0.90)\n", + "\n", + "m.fs.R101.volume.setlb(0 * pyunits.m**3)\n", + "m.fs.R101.volume.setub(pyunits.convert(5000 * pyunits.gal, to_units=pyunits.m**3))\n", + "\n", + "m.fs.R101.length.unfix()\n", + "m.fs.R101.length.setlb(0 * pyunits.m)\n", + "m.fs.R101.length.setub(5 * pyunits.m)\n", + "\n", + "m.fs.H101.outlet.temperature.unfix()\n", + "m.fs.H101.outlet.temperature[0].setlb(328.15 * pyunits.K)\n", + "m.fs.H101.outlet.temperature[0].setub(\n", + " 470.45 * pyunits.K\n", + ") # highest component boiling point (ethylene glycol)\n", + "\n", + "for x in m.fs.R101.control_volume.length_domain:\n", + " if x == 0:\n", + " continue\n", + " m.fs.R101.control_volume.properties[\n", + " 0, x\n", + " ].temperature.unfix() # allow for temperature change in each finite element" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "We have now defined the optimization problem and we are now ready to solve this problem. \n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "results = solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", + "\n", + "print()\n", + "print(\"Heater results\")\n", + "\n", + "m.fs.H101.report()\n", + "\n", + "print()\n", + "print(\"PFR reactor results\")\n", + "\n", + "m.fs.R101.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display optimal values for the decision variables and design variables:" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Optimal Values\")\n", + "print()\n", + "\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", + "\n", + "print()\n", + "print(\n", + " \"Total heat duty required = \",\n", + " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", + " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", + " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.6f}\"\"\"\n", + " f\" MJ\",\n", + ")\n", + "print()\n", + "print(f\"Tube area required = {value(m.fs.R101.area):0.6f} m^2\")\n", + "\n", + "print()\n", + "print(f\"Tube length required = {value(m.fs.R101.length):0.6f} m\")\n", + "\n", + "print()\n", + "print(\n", + " f\"Assuming a 20% design factor for reactor volume,\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume, to_units=pyunits.gal)):0.6f} gal\"\n", + ")\n", + "\n", + "print()\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Tags", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" } - ], - "source": [ - "print(\"Optimal Values\")\n", - "print()\n", - "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", - "\n", - "print()\n", - "print(\n", - " \"Total heat duty required = \",\n", - " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", - " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", - " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.3f}\"\"\"\n", - " f\" MJ\",\n", - ")\n", - "print()\n", - "print(f\"Tube area required = {value(m.fs.R101.area):0.3f} m^2\")\n", - "\n", - "print()\n", - "print(f\"Tube length required = {value(m.fs.R101.length):0.3f} m\")\n", - "\n", - "print()\n", - "print(\n", - " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume, to_units=pyunits.gal)):0.3f} gal\"\n", - ")\n", - "\n", - "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "celltoolbar": "Tags", - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 3 -} + "nbformat": 4, + "nbformat_minor": 3 +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_test.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_test.ipynb index dcfe84f8..76d32312 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_test.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_test.ipynb @@ -53,7 +53,7 @@ "\n", "Chemical reaction:\n", "\n", - "**C2H4O + H2O + H2SO4 → C2H6O2 + H2SO4**\n", + "**C2H4O + H2O + H2SO4 \u2192 C2H6O2 + H2SO4**\n", "\n", "Property Packages:\n", "\n", @@ -626,7 +626,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" ] }, { @@ -641,7 +641,7 @@ "source": [ "import pytest\n", "\n", - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(2.082, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(6.589, rel=1e-5)" ] }, { @@ -667,15 +667,15 @@ " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * \n", " (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", - " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.3f}\"\"\"\n", + " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.6f}\"\"\"\n", " f\" MJ\"\n", ")\n", "print()\n", - "print(f\"Tube area required = {value(m.fs.R101.area):0.3f} m^2\")\n", + "print(f\"Tube area required = {value(m.fs.R101.area):0.6f} m^2\")\n", "print()\n", - "print(f\"Tube length required = {value(m.fs.R101.length):0.3f} m\")\n", + "print(f\"Tube length required = {value(m.fs.R101.length):0.6f} m\")\n", "print()\n", - "print(f\"Tube volume required = {value(m.fs.R101.volume):0.3f} m^3\")" + "print(f\"Tube volume required = {value(m.fs.R101.volume):0.6f} m^3\")" ] }, { @@ -688,8 +688,8 @@ }, "outputs": [], "source": [ - "assert value(m.fs.R101.conversion) == pytest.approx(0.5000, abs=1e-3)\n", - "assert value(m.fs.R101.area) == pytest.approx(1.1490, abs=1e-3)\n", + "assert value(m.fs.R101.conversion) == pytest.approx(0.5000, rel=1e-5)\n", + "assert value(m.fs.R101.area) == pytest.approx(0.987071, rel=1e-5)\n", "assert (\n", " value(m.fs.R101.length)\n", " / value(m.fs.R101.config.finite_elements)\n", @@ -705,8 +705,8 @@ " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)])\n", " )\n", " / 2\n", - ") / 1e6 == pytest.approx(-4.734, abs=1e-3)\n", - "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2815, abs=1e-3)" + ") / 1e6 == pytest.approx(-4.881815, rel=1e-5)\n", + "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2815, rel=1e-5)" ] }, { @@ -829,7 +829,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", "\n", "print()\n", "print(\"Heater results\")\n", @@ -852,8 +852,8 @@ }, "outputs": [], "source": [ - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(0.3184, abs=1e-3)\n", - "assert value(m.fs.R101.area) == pytest.approx(2.0870, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(4.421530, rel=1e-5)\n", + "assert value(m.fs.R101.area) == pytest.approx(2.9300, rel=1e-5)" ] }, { @@ -872,31 +872,31 @@ "print(\"Optimal Values\")\n", "print()\n", "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", "print(\n", " \"Total heat duty required = \",\n", " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", - " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.3f}\"\"\"\n", + " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.6f}\"\"\"\n", " f\" MJ\",\n", ")\n", "print()\n", - "print(f\"Tube area required = {value(m.fs.R101.area):0.3f} m^2\")\n", + "print(f\"Tube area required = {value(m.fs.R101.area):0.6f} m^2\")\n", "\n", "print()\n", - "print(f\"Tube length required = {value(m.fs.R101.length):0.3f} m\")\n", + "print(f\"Tube length required = {value(m.fs.R101.length):0.6f} m\")\n", "\n", "print()\n", "print(\n", " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume, to_units=pyunits.gal)):0.3f} gal\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume, to_units=pyunits.gal)):0.6f} gal\"\n", ")\n", "\n", "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", "\n", "print()\n", "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" @@ -912,7 +912,7 @@ }, "outputs": [], "source": [ - "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, abs=1e-3)\n", + "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, rel=1e-5)\n", "assert (\n", " value(m.fs.R101.length)\n", " / value(m.fs.R101.config.finite_elements)\n", @@ -932,12 +932,12 @@ " )\n", " / 2\n", " )\n", - ") / 1e6 == pytest.approx(-3.440, abs=1e-3)\n", - "assert value(m.fs.R101.area) == pytest.approx(2.0870, abs=1e-3)\n", - "assert value(m.fs.R101.control_volume.length) == pytest.approx(4.9788, abs=1e-3)\n", - "assert value(m.fs.R101.volume * 1.2) == pytest.approx(12.469, abs=1e-3)\n", - "assert value(m.fs.eg_prod) == pytest.approx(225.415, abs=1e-3)\n", - "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.000, abs=1e-3)" + ") / 1e6 == pytest.approx(-3.789565, rel=1e-5)\n", + "assert value(m.fs.R101.area) == pytest.approx(2.930001, rel=1e-5)\n", + "assert value(m.fs.R101.control_volume.length) == pytest.approx(4.982470, rel=1e-5)\n", + "assert value(m.fs.R101.volume * 1.2) == pytest.approx(17.518369, rel=1e-5)\n", + "assert value(m.fs.eg_prod) == pytest.approx(225.415471, rel=1e-5)\n", + "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.000, rel=1e-5)" ] }, { @@ -972,9 +972,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.16" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_usr.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_usr.ipynb index b0473332..30984465 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_usr.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/plug_flow_reactor_usr.ipynb @@ -53,7 +53,7 @@ "\n", "Chemical reaction:\n", "\n", - "**C2H4O + H2O + H2SO4 → C2H6O2 + H2SO4**\n", + "**C2H4O + H2O + H2SO4 \u2192 C2H6O2 + H2SO4**\n", "\n", "Property Packages:\n", "\n", @@ -582,7 +582,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" ] }, { @@ -608,15 +608,15 @@ " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * \n", " (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", - " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.3f}\"\"\"\n", + " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.6f}\"\"\"\n", " f\" MJ\"\n", ")\n", "print()\n", - "print(f\"Tube area required = {value(m.fs.R101.area):0.3f} m^2\")\n", + "print(f\"Tube area required = {value(m.fs.R101.area):0.6f} m^2\")\n", "print()\n", - "print(f\"Tube length required = {value(m.fs.R101.length):0.3f} m\")\n", + "print(f\"Tube length required = {value(m.fs.R101.length):0.6f} m\")\n", "print()\n", - "print(f\"Tube volume required = {value(m.fs.R101.volume):0.3f} m^3\")" + "print(f\"Tube volume required = {value(m.fs.R101.volume):0.6f} m^3\")" ] }, { @@ -710,7 +710,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", "\n", "print()\n", "print(\"Heater results\")\n", @@ -739,31 +739,31 @@ "print(\"Optimal Values\")\n", "print()\n", "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", "print(\n", " \"Total heat duty required = \",\n", " f\"\"\"{(value(m.fs.R101.length) / value(m.fs.R101.config.finite_elements) * (value(sum(m.fs.R101.heat_duty[0, k] for k in m.fs.R101.control_volume.length_domain if 0.0 <= k < 1.0))\n", " + (value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(1)])\n", - " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.3f}\"\"\"\n", + " + value(m.fs.R101.heat_duty[0, m.fs.R101.control_volume.length_domain.at(-1)]))/2))/1e6:0.6f}\"\"\"\n", " f\" MJ\",\n", ")\n", "print()\n", - "print(f\"Tube area required = {value(m.fs.R101.area):0.3f} m^2\")\n", + "print(f\"Tube area required = {value(m.fs.R101.area):0.6f} m^2\")\n", "\n", "print()\n", - "print(f\"Tube length required = {value(m.fs.R101.length):0.3f} m\")\n", + "print(f\"Tube length required = {value(m.fs.R101.length):0.6f} m\")\n", "\n", "print()\n", "print(\n", " f\"Assuming a 20% design factor for reactor volume,\"\n", - " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume):0.3f}\"\n", - " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume, to_units=pyunits.gal)):0.3f} gal\"\n", + " f\"total CSTR volume required = {value(1.2*m.fs.R101.volume):0.6f}\"\n", + " f\" m^3 = {value(pyunits.convert(1.2*m.fs.R101.volume, to_units=pyunits.gal)):0.6f} gal\"\n", ")\n", "\n", "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", "\n", "print()\n", "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" @@ -801,9 +801,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.16" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor.ipynb index c12fd541..b0ed3611 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor.ipynb @@ -219,9 +219,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = StoichiometricReactor(\n", @@ -564,7 +562,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" ] }, { @@ -579,7 +577,7 @@ "source": [ "import pytest\n", "\n", - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(3.458140, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(8.019605, rel=1e-5)" ] }, { @@ -611,9 +609,9 @@ }, "outputs": [], "source": [ - "assert value(m.fs.R101.conversion) == pytest.approx(0.8000, abs=1e-3)\n", - "assert value(m.fs.R101.heat_duty[0]) / 1e6 == pytest.approx(-5.6566, abs=1e-3)\n", - "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2815, abs=1e-3)" + "assert value(m.fs.R101.conversion) == pytest.approx(0.8000, rel=1e-5)\n", + "assert value(m.fs.R101.heat_duty[0]) / 1e6 == pytest.approx(-5.8931, rel=1e-5)\n", + "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2815, rel=1e-5)" ] }, { @@ -724,7 +722,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", "\n", "print()\n", "print(\"Heater results\")\n", @@ -747,7 +745,7 @@ }, "outputs": [], "source": [ - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(3.888050, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(6.670729, rel=1e-5)" ] }, { @@ -766,13 +764,13 @@ "print(\"Optimal Values\")\n", "print()\n", "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", - "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", "\n", "print()\n", "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" @@ -788,10 +786,10 @@ }, "outputs": [], "source": [ - "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, abs=1e-3)\n", - "assert value(m.fs.R101.outlet.temperature[0]) / 100 == pytest.approx(4.5000, abs=1e-3)\n", - "assert value(m.fs.eg_prod) == pytest.approx(225.415, abs=1e-3)\n", - "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.0, abs=1e-3)" + "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, rel=1e-5)\n", + "assert value(m.fs.R101.outlet.temperature[0]) / 100 == pytest.approx(4.5000, rel=1e-5)\n", + "assert value(m.fs.eg_prod) == pytest.approx(225.415, rel=1e-5)\n", + "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.0, rel=1e-5)" ] }, { @@ -819,7 +817,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_doc.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_doc.ipynb index d7b699c7..99443416 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_doc.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_doc.ipynb @@ -1,1210 +1,693 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "tags": [ - "header", - "hide-cell" - ] - }, - "outputs": [], - "source": [ - "###############################################################################\n", - "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", - "# Framework (IDAES IP) was produced under the DOE Institute for the\n", - "# Design of Advanced Energy Systems (IDAES).\n", - "#\n", - "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", - "# University of California, through Lawrence Berkeley National Laboratory,\n", - "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", - "# University, West Virginia University Research Corporation, et al.\n", - "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", - "# for full copyright and license information.\n", - "###############################################################################" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "# Flowsheet Stoichiometric Reactor Simulation and Optimization of Ethylene Glycol Production\n", - "Author: Brandon Paul \n", - "Maintainer: Brandon Paul \n", - "Updated: 2023-06-01 \n", - "\n", - "## Learning Outcomes\n", - "\n", - "\n", - "- Call and implement the IDAES StochiometricReactor unit model\n", - "- Construct a steady-state flowsheet using the IDAES unit model library\n", - "- Connecting unit models in a flowsheet using Arcs\n", - "- Fomulate and solve an optimization problem\n", - " - Defining an objective function\n", - " - Setting variable bounds\n", - " - Adding additional constraints \n", - "\n", - "\n", - "## Problem Statement\n", - "\n", - "Following the previous example implementing a [Continuous Stirred Tank Reactor (CSTR) unit model](http://localhost:8888/notebooks/GitHub/examples-pse/src/Examples/UnitModels/Reactors/cstr_testing_doc.md), we can alter the flowsheet to use a stochiometric (or yield) reactor. As before, this example is adapted from Fogler, H.S., Elements of Chemical Reaction Engineering 5th ed., 2016, Prentice Hall, p. 157-160 with the following chemical reaction, property packages and flowsheet. Unlike the previous two reactors which apply performance equations to calculate reaction extent, this simplified reactor model neglects all geometric properties and allows the user to specify a yield per reaction. The state variables chosen for the property package are **molar flows of each component by phase in each stream, temperature of each stream and pressure of each stream**. The components considered are: **ethylene oxide, water, sulfuric acid and ethylene glycol** and the process occurs in liquid phase only. Therefore, every stream has 4 flow variables, 1 temperature and 1 pressure variable.\n", - "\n", - "Chemical reaction:\n", - "\n", - "**C2H4O + H2O + H2SO4 → C2H6O2 + H2SO4**\n", - "\n", - "Property Packages:\n", - "\n", - "- egprod_ideal.py\n", - "- egprod_reaction.py\n", - "\n", - "Flowsheet:\n", - "\n", - "![](egprod_flowsheet.png)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Pyomo and IDAES components\n", - "\n", - "\n", - "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages. Let us first import the following components from Pyomo:\n", - "- Constraint (to write constraints)\n", - "- Var (to declare variables)\n", - "- ConcreteModel (to create the concrete model object)\n", - "- Expression (to evaluate values as a function of variables defined in the model)\n", - "- Objective (to define an objective function for optimization)\n", - "- TransformationFactory (to apply certain transformations)\n", - "- Arc (to connect two unit models)\n", - "\n", - "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\n", - "\n", - "From idaes, we will be needing the `FlowsheetBlock` and the following unit models:\n", - "- Mixer\n", - "- Heater\n", - "- StoichiometricReactor\n", - "\n", - "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom, tools for model expressions and calling variable values, and built-in functions to define property packages, add unit containers to objects and define our initialization scheme.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "from pyomo.environ import (\n", - " Constraint,\n", - " Var,\n", - " ConcreteModel,\n", - " Expression,\n", - " Objective,\n", - " TransformationFactory,\n", - " value,\n", - " units as pyunits,\n", - ")\n", - "from pyomo.network import Arc\n", - "\n", - "from idaes.core import FlowsheetBlock\n", - "from idaes.models.properties.modular_properties.base.generic_property import (\n", - " GenericParameterBlock,\n", - ")\n", - "from idaes.models.properties.modular_properties.base.generic_reaction import (\n", - " GenericReactionParameterBlock,\n", - ")\n", - "from idaes.models.unit_models import Feed, Mixer, Heater, StoichiometricReactor, Product\n", - "\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.util.initialization import propagate_state" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Importing Required Thermophysical and Reaction Packages\n", - "\n", - "The final step is to import the thermophysical and reaction packages. We have created a custom thermophysical package that support ideal vapor and liquid behavior for this system, and in this case we will restrict it to ideal liquid behavior only. \n", - "\n", - "Let us import the following modules from the same directory as this Jupyter notebook:\n", - "- egprod_ideal as thermo_props\n", - "- egprod_reaction as reaction_props" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "import egprod_ideal as thermo_props\n", - "import egprod_reaction as reaction_props" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Constructing the Flowsheet\n", - "\n", - "We have now imported all the components, unit models, and property modules we need to construct a flowsheet. Let us create a ConcreteModel and add the flowsheet block. " - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "m = ConcreteModel()\n", - "m.fs = FlowsheetBlock(dynamic=False)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now need to add the property packages to the flowsheet. Unlike the basic [Flash unit model example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Tutorials/Basics/flash_unit_solution_testing_doc.md), where we only had a thermophysical property package, for this flowsheet we will also need to add a reaction property package. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) and [Modular Reaction Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-reaction-package-framework). The get_prop method for the natural gas property module automatically returns the correct dictionary using a component list argument. The GenericParameterBlock and GenericReactionParameterBlock methods build states blocks from passed parameter data; the reaction block unpacks using **reaction_props.config_dict to allow for optional or empty keyword arguments:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.fs.thermo_params = GenericParameterBlock(**thermo_props.config_dict)\n", - "m.fs.reaction_params = GenericReactionParameterBlock(\n", - " property_package=m.fs.thermo_params, **reaction_props.config_dict\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Unit Models\n", - "\n", - "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding a `Mixer`, a `Heater` and a `StoichiometricReactor`. Note that all unit models need to be given a property package argument. In addition to that, there are several arguments depending on the unit model, please refer to the documentation for more details on [IDAES Unit Models](https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/index.html). For example, the `Mixer` is given a `list` consisting of names to the two inlets." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "m.fs.OXIDE = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.ACID = Feed(property_package=m.fs.thermo_params)\n", - "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", - "m.fs.M101 = Mixer(\n", - " property_package=m.fs.thermo_params, inlet_list=[\"reagent_feed\", \"catalyst_feed\"]\n", - ")\n", - "m.fs.H101 = Heater(\n", - " property_package=m.fs.thermo_params,\n", - " has_pressure_change=False,\n", - " has_phase_equilibrium=False,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "m.fs.R101 = StoichiometricReactor(\n", - " property_package=m.fs.thermo_params,\n", - " reaction_package=m.fs.reaction_params,\n", - " has_heat_of_reaction=True,\n", - " has_heat_transfer=True,\n", - " has_pressure_change=False,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Connecting Unit Models Using Arcs\n", - "\n", - "We have now added all the unit models we need to the flowsheet. However, we have not yet specified how the units are to be connected. To do this, we will be using the `Arc` which is a pyomo component that takes in two arguments: `source` and `destination`. Let us connect the outlet of the `Mixer` to the inlet of the `Heater`, and the outlet of the `Heater` to the inlet of the `StoichiometricReactor`. Additionally, we will connect the `Feed` and `Product` blocks to the flowsheet:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.s01 = Arc(source=m.fs.OXIDE.outlet, destination=m.fs.M101.reagent_feed)\n", - "m.fs.s02 = Arc(source=m.fs.ACID.outlet, destination=m.fs.M101.catalyst_feed)\n", - "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)\n", - "m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", - "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We have now connected the unit model block using the arcs. However, we also need to link the state variables on connected ports. Pyomo provides a convenient method `TransformationFactory` to write these equality constraints for us between two ports:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "TransformationFactory(\"network.expand_arcs\").apply_to(m)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding Expressions to Compute Operating Costs\n", - "\n", - "In this section, we will add a few Expressions that allows us to evaluate the performance. `Expressions` provide a convenient way of calculating certain values that are a function of the variables defined in the model. For more details on `Expressions`, please refer to the [Pyomo Expression documentation]( https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html).\n", - "\n", - "For this flowsheet, we are interested in computing ethylene glycol production in millions of pounds per year, as well as the total costs due to cooling and heating utilities." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us first add an `Expression` to convert the product flow from mol/s to MM lb/year of ethylene glycol. We see that the molecular weight exists in the thermophysical property package, so we may use that value for our calculations." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.eg_prod = Expression(\n", - " expr=pyunits.convert(\n", - " m.fs.PROD.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", - " * m.fs.thermo_params.ethylene_glycol.mw, # MW defined in properties as kg/mol\n", - " to_units=pyunits.Mlb / pyunits.yr,\n", - " )\n", - ") # converting kg/s to MM lb/year" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, let us add expressions to compute the reactor cooling cost (\\\\$/s) assuming a cost of 2.12E-5 \\\\$/kW, and the heating utility cost (\\\\$/s) assuming 2.2E-4 \\\\$/kW. Note that the heat duty is in units of watt (J/s). The total operating cost will be the sum of the two, expressed in \\\\$/year assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.cooling_cost = Expression(\n", - " expr=2.12e-8 * (-m.fs.R101.heat_duty[0])\n", - ") # the reaction is exothermic, so R101 duty is negative\n", - "m.fs.heating_cost = Expression(\n", - " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", - ") # the stream must be heated to T_rxn, so H101 duty is positive\n", - "m.fs.operating_cost = Expression(\n", - " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost))\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Feed Conditions\n", - "\n", - "Let us first check how many degrees of freedom exist for this flowsheet using the `degrees_of_freedom` tool we imported earlier. We expect each stream to have 6 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 1 (duty or overall conversion, since the inlet is also the outlet of H101). In this case, the reactor has an extra degree of freedom since we have not yet defined the yield of the sole rate-kinetics reaction. Therefore, we have 15 degrees of freedom to specify: temperature, pressure and flow of all four components on both streams; outlet heater temperature; reactor conversion and duty." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "15\n" - ] + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "header", + "hide-cell" + ] + }, + "outputs": [], + "source": [ + "###############################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "###############################################################################" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "# Flowsheet Stoichiometric Reactor Simulation and Optimization of Ethylene Glycol Production\n", + "Author: Brandon Paul \n", + "Maintainer: Brandon Paul \n", + "Updated: 2023-06-01 \n", + "\n", + "## Learning Outcomes\n", + "\n", + "\n", + "- Call and implement the IDAES StochiometricReactor unit model\n", + "- Construct a steady-state flowsheet using the IDAES unit model library\n", + "- Connecting unit models in a flowsheet using Arcs\n", + "- Fomulate and solve an optimization problem\n", + " - Defining an objective function\n", + " - Setting variable bounds\n", + " - Adding additional constraints \n", + "\n", + "\n", + "## Problem Statement\n", + "\n", + "Following the previous example implementing a [Continuous Stirred Tank Reactor (CSTR) unit model](http://localhost:8888/notebooks/GitHub/examples-pse/src/Examples/UnitModels/Reactors/cstr_testing_doc.md), we can alter the flowsheet to use a stochiometric (or yield) reactor. As before, this example is adapted from Fogler, H.S., Elements of Chemical Reaction Engineering 5th ed., 2016, Prentice Hall, p. 157-160 with the following chemical reaction, property packages and flowsheet. Unlike the previous two reactors which apply performance equations to calculate reaction extent, this simplified reactor model neglects all geometric properties and allows the user to specify a yield per reaction. The state variables chosen for the property package are **molar flows of each component by phase in each stream, temperature of each stream and pressure of each stream**. The components considered are: **ethylene oxide, water, sulfuric acid and ethylene glycol** and the process occurs in liquid phase only. Therefore, every stream has 4 flow variables, 1 temperature and 1 pressure variable.\n", + "\n", + "Chemical reaction:\n", + "\n", + "**C2H4O + H2O + H2SO4 \u2192 C2H6O2 + H2SO4**\n", + "\n", + "Property Packages:\n", + "\n", + "- egprod_ideal.py\n", + "- egprod_reaction.py\n", + "\n", + "Flowsheet:\n", + "\n", + "![](egprod_flowsheet.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Pyomo and IDAES components\n", + "\n", + "\n", + "To construct a flowsheet, we will need several components from the Pyomo and IDAES packages. Let us first import the following components from Pyomo:\n", + "- Constraint (to write constraints)\n", + "- Var (to declare variables)\n", + "- ConcreteModel (to create the concrete model object)\n", + "- Expression (to evaluate values as a function of variables defined in the model)\n", + "- Objective (to define an objective function for optimization)\n", + "- TransformationFactory (to apply certain transformations)\n", + "- Arc (to connect two unit models)\n", + "\n", + "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\n", + "\n", + "From idaes, we will be needing the `FlowsheetBlock` and the following unit models:\n", + "- Mixer\n", + "- Heater\n", + "- StoichiometricReactor\n", + "\n", + "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom, tools for model expressions and calling variable values, and built-in functions to define property packages, add unit containers to objects and define our initialization scheme.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pyomo.environ import (\n", + " Constraint,\n", + " Var,\n", + " ConcreteModel,\n", + " Expression,\n", + " Objective,\n", + " TransformationFactory,\n", + " value,\n", + " units as pyunits,\n", + ")\n", + "from pyomo.network import Arc\n", + "\n", + "from idaes.core import FlowsheetBlock\n", + "from idaes.models.properties.modular_properties.base.generic_property import (\n", + " GenericParameterBlock,\n", + ")\n", + "from idaes.models.properties.modular_properties.base.generic_reaction import (\n", + " GenericReactionParameterBlock,\n", + ")\n", + "from idaes.models.unit_models import Feed, Mixer, Heater, StoichiometricReactor, Product\n", + "\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.util.initialization import propagate_state" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing Required Thermophysical and Reaction Packages\n", + "\n", + "The final step is to import the thermophysical and reaction packages. We have created a custom thermophysical package that support ideal vapor and liquid behavior for this system, and in this case we will restrict it to ideal liquid behavior only. \n", + "\n", + "Let us import the following modules from the same directory as this Jupyter notebook:\n", + "- egprod_ideal as thermo_props\n", + "- egprod_reaction as reaction_props" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import egprod_ideal as thermo_props\n", + "import egprod_reaction as reaction_props" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Constructing the Flowsheet\n", + "\n", + "We have now imported all the components, unit models, and property modules we need to construct a flowsheet. Let us create a ConcreteModel and add the flowsheet block. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = ConcreteModel()\n", + "m.fs = FlowsheetBlock(dynamic=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now need to add the property packages to the flowsheet. Unlike the basic [Flash unit model example](http://localhost:8888/notebooks/GitHub/examples-pse/src/Tutorials/Basics/flash_unit_solution_testing_doc.md), where we only had a thermophysical property package, for this flowsheet we will also need to add a reaction property package. We will use the [Modular Property Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-property-package-framework) and [Modular Reaction Framework](https://idaes-pse.readthedocs.io/en/stable/explanations/components/property_package/index.html#generic-reaction-package-framework). The get_prop method for the natural gas property module automatically returns the correct dictionary using a component list argument. The GenericParameterBlock and GenericReactionParameterBlock methods build states blocks from passed parameter data; the reaction block unpacks using **reaction_props.config_dict to allow for optional or empty keyword arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "m.fs.thermo_params = GenericParameterBlock(**thermo_props.config_dict)\n", + "m.fs.reaction_params = GenericReactionParameterBlock(\n", + " property_package=m.fs.thermo_params, **reaction_props.config_dict\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Unit Models\n", + "\n", + "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding a `Mixer`, a `Heater` and a `StoichiometricReactor`. Note that all unit models need to be given a property package argument. In addition to that, there are several arguments depending on the unit model, please refer to the documentation for more details on [IDAES Unit Models](https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/index.html). For example, the `Mixer` is given a `list` consisting of names to the two inlets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "m.fs.OXIDE = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.ACID = Feed(property_package=m.fs.thermo_params)\n", + "m.fs.PROD = Product(property_package=m.fs.thermo_params)\n", + "m.fs.M101 = Mixer(\n", + " property_package=m.fs.thermo_params, inlet_list=[\"reagent_feed\", \"catalyst_feed\"]\n", + ")\n", + "m.fs.H101 = Heater(\n", + " property_package=m.fs.thermo_params,\n", + " has_pressure_change=False,\n", + " has_phase_equilibrium=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101 = StoichiometricReactor(\n", + " property_package=m.fs.thermo_params,\n", + " reaction_package=m.fs.reaction_params,\n", + " has_heat_of_reaction=True,\n", + " has_heat_transfer=True,\n", + " has_pressure_change=False,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Connecting Unit Models Using Arcs\n", + "\n", + "We have now added all the unit models we need to the flowsheet. However, we have not yet specified how the units are to be connected. To do this, we will be using the `Arc` which is a pyomo component that takes in two arguments: `source` and `destination`. Let us connect the outlet of the `Mixer` to the inlet of the `Heater`, and the outlet of the `Heater` to the inlet of the `StoichiometricReactor`. Additionally, we will connect the `Feed` and `Product` blocks to the flowsheet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.s01 = Arc(source=m.fs.OXIDE.outlet, destination=m.fs.M101.reagent_feed)\n", + "m.fs.s02 = Arc(source=m.fs.ACID.outlet, destination=m.fs.M101.catalyst_feed)\n", + "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)\n", + "m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)\n", + "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.PROD.inlet)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have now connected the unit model block using the arcs. However, we also need to link the state variables on connected ports. Pyomo provides a convenient method `TransformationFactory` to write these equality constraints for us between two ports:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "TransformationFactory(\"network.expand_arcs\").apply_to(m)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adding Expressions to Compute Operating Costs\n", + "\n", + "In this section, we will add a few Expressions that allows us to evaluate the performance. `Expressions` provide a convenient way of calculating certain values that are a function of the variables defined in the model. For more details on `Expressions`, please refer to the [Pyomo Expression documentation]( https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html).\n", + "\n", + "For this flowsheet, we are interested in computing ethylene glycol production in millions of pounds per year, as well as the total costs due to cooling and heating utilities." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us first add an `Expression` to convert the product flow from mol/s to MM lb/year of ethylene glycol. We see that the molecular weight exists in the thermophysical property package, so we may use that value for our calculations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.eg_prod = Expression(\n", + " expr=pyunits.convert(\n", + " m.fs.PROD.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"]\n", + " * m.fs.thermo_params.ethylene_glycol.mw, # MW defined in properties as kg/mol\n", + " to_units=pyunits.Mlb / pyunits.yr,\n", + " )\n", + ") # converting kg/s to MM lb/year" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let us add expressions to compute the reactor cooling cost (\\\\$/s) assuming a cost of 2.12E-5 \\\\$/kW, and the heating utility cost (\\\\$/s) assuming 2.2E-4 \\\\$/kW. Note that the heat duty is in units of watt (J/s). The total operating cost will be the sum of the two, expressed in \\\\$/year assuming 8000 operating hours per year (~10\\% downtime, which is fairly common for small scale chemical plants):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.cooling_cost = Expression(\n", + " expr=2.12e-8 * (-m.fs.R101.heat_duty[0])\n", + ") # the reaction is exothermic, so R101 duty is negative\n", + "m.fs.heating_cost = Expression(\n", + " expr=2.2e-7 * m.fs.H101.heat_duty[0]\n", + ") # the stream must be heated to T_rxn, so H101 duty is positive\n", + "m.fs.operating_cost = Expression(\n", + " expr=(3600 * 8000 * (m.fs.heating_cost + m.fs.cooling_cost))\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Feed Conditions\n", + "\n", + "Let us first check how many degrees of freedom exist for this flowsheet using the `degrees_of_freedom` tool we imported earlier. We expect each stream to have 6 degrees of freedom, the mixer to have 0 (after both streams are accounted for), the heater to have 1 (just the duty, since the inlet is also the outlet of M101), and the reactor to have 1 (duty or overall conversion, since the inlet is also the outlet of H101). In this case, the reactor has an extra degree of freedom since we have not yet defined the yield of the sole rate-kinetics reaction. Therefore, we have 15 degrees of freedom to specify: temperature, pressure and flow of all four components on both streams; outlet heater temperature; reactor conversion and duty." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "print(degrees_of_freedom(m))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now be fixing the feed stream to the conditions shown in the flowsheet above. As mentioned in other tutorials, the IDAES framework expects a time index value for every referenced internal stream or unit variable, even in steady-state systems with a single time point $ t = 0 $ (`t = [0]` is the default when creating a `FlowsheetBlock` without passing a `time_set` argument). The non-present components in each stream are assigned a very small non-zero value to help with convergence and initializing. Based on stoichiometric ratios for the reaction, 80% conversion and 200 MM lb/year (46.4 mol/s) of ethylene glycol, we will initialize our simulation with the following calculated values:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", + " 58.0 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", + " 39.6 * pyunits.mol / pyunits.s\n", + ") # calculated from 16.1 mol EO / cudm in stream\n", + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.OXIDE.outlet.temperature.fix(298.15 * pyunits.K)\n", + "m.fs.OXIDE.outlet.pressure.fix(1e5 * pyunits.Pa)\n", + "\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", + " 200 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", + " 0.334 * pyunits.mol / pyunits.s\n", + ") # calculated from 0.9 wt% SA in stream\n", + "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", + " 1e-5 * pyunits.mol / pyunits.s\n", + ")\n", + "m.fs.ACID.outlet.temperature.fix(298.15 * pyunits.K)\n", + "m.fs.ACID.outlet.pressure.fix(1e5 * pyunits.Pa)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Fixing Unit Model Specifications\n", + "\n", + "Now that we have fixed our inlet feed conditions, we will now be fixing the operating conditions for the unit models in the flowsheet. Let us fix the outlet temperature of H101 to 328.15 K. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.H101.outlet.temperature.fix(328.15 * pyunits.K)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will need to specify both initial reactant extent (conversion or yield) and heat duty values (these are the only two free variables to choose from). The reaction extent can be specified directly, as a molar or mass yield ratio of product to a particular reactant, or fractional conversion of a particular reactant. Here, we choose fractional conversion in terms of ethylene oxide. Since heat duty and the outlet reactor temperature are interdependent, we can choose to specify this quantity instead. While the reaction kinetic parameters exist in the property package, we also do not need to add a rate constant expression since generation is explicitly defined through the conversion/yield. Note that our initial problem will solve with zero *temperature change* but will be infeasible with zero *heat duty*; this is due to the heat of reaction enforced by allowing heat transfer and mandating a non-zero conversion." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.conversion = Var(\n", + " initialize=0.80, bounds=(0, 1), units=pyunits.dimensionless\n", + ") # fraction\n", + "\n", + "m.fs.R101.conv_constraint = Constraint(\n", + " expr=m.fs.R101.conversion\n", + " * m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", + " == (\n", + " m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", + " - m.fs.R101.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", + " )\n", + ")\n", + "\n", + "m.fs.R101.conversion.fix(0.80)\n", + "\n", + "m.fs.R101.outlet.temperature.fix(328.15 * pyunits.K) # equal inlet reactor temperature" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(degrees_of_freedom(m))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we need to initialize the each unit operation in sequence to solve the flowsheet. As in best practice, unit operations are initialized or solved, and outlet properties are propagated to connected inlet streams via arc definitions as follows:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize and solve each unit operation\n", + "m.fs.OXIDE.initialize()\n", + "propagate_state(arc=m.fs.s01)\n", + "\n", + "m.fs.ACID.initialize()\n", + "propagate_state(arc=m.fs.s01)\n", + "\n", + "m.fs.M101.initialize()\n", + "propagate_state(arc=m.fs.s03)\n", + "\n", + "m.fs.H101.initialize()\n", + "propagate_state(arc=m.fs.s04)\n", + "\n", + "m.fs.R101.initialize()\n", + "propagate_state(arc=m.fs.s05)\n", + "\n", + "m.fs.PROD.initialize()\n", + "\n", + "# set solver\n", + "solver = get_solver()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# Solve the model\n", + "results = solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Analyze the Results of the Square Problem\n", + "\n", + "\n", + "What is the total operating cost? " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this operating cost, what conversion did we achieve of ethylene oxide to ethylene glycol? " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.R101.report()\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Optimizing Ethylene Glycol Production\n", + "\n", + "Now that the flowsheet has been squared and solved, we can run a small optimization problem to minimize our production costs. Suppose we require at least 200 million pounds/year of ethylene glycol produced and 90% conversion of ethylene oxide, allowing for variable and reactor temperature (heater outlet)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us declare our objective function for this problem. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.objective = Objective(expr=m.fs.operating_cost)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now, as well as set bounds for the design variables (reactor outlet temperature is set by state variable bounds in property package):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.eg_prod_con = Constraint(\n", + " expr=m.fs.eg_prod >= 200 * pyunits.Mlb / pyunits.yr\n", + ") # MM lb/year\n", + "m.fs.R101.conversion.fix(0.90)\n", + "\n", + "m.fs.H101.outlet.temperature.unfix()\n", + "m.fs.H101.outlet.temperature[0].setlb(328.15 * pyunits.K)\n", + "m.fs.H101.outlet.temperature[0].setub(\n", + " 470.45 * pyunits.K\n", + ") # highest component boiling point (ethylene glycol)\n", + "\n", + "m.fs.R101.outlet.temperature.unfix()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "We have now defined the optimization problem and we are now ready to solve this problem. \n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "results = solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", + "\n", + "print()\n", + "print(\"Heater results\")\n", + "\n", + "m.fs.H101.report()\n", + "\n", + "print()\n", + "print(\"Stoichiometric reactor results\")\n", + "\n", + "m.fs.R101.report()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Display optimal values for the decision variables and design variables:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"Optimal Values\")\n", + "print()\n", + "\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", + "\n", + "print()\n", + "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.6f} K\")\n", + "\n", + "print()\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", + "\n", + "print()\n", + "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will now be fixing the feed stream to the conditions shown in the flowsheet above. As mentioned in other tutorials, the IDAES framework expects a time index value for every referenced internal stream or unit variable, even in steady-state systems with a single time point $ t = 0 $ (`t = [0]` is the default when creating a `FlowsheetBlock` without passing a `time_set` argument). The non-present components in each stream are assigned a very small non-zero value to help with convergence and initializing. Based on stoichiometric ratios for the reaction, 80% conversion and 200 MM lb/year (46.4 mol/s) of ethylene glycol, we will initialize our simulation with the following calculated values:" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", - " 58.0 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", - " 39.6 * pyunits.mol / pyunits.s\n", - ") # calculated from 16.1 mol EO / cudm in stream\n", - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.OXIDE.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.OXIDE.outlet.temperature.fix(298.15 * pyunits.K)\n", - "m.fs.OXIDE.outlet.pressure.fix(1e5 * pyunits.Pa)\n", - "\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"water\"].fix(\n", - " 200 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"sulfuric_acid\"].fix(\n", - " 0.334 * pyunits.mol / pyunits.s\n", - ") # calculated from 0.9 wt% SA in stream\n", - "m.fs.ACID.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_glycol\"].fix(\n", - " 1e-5 * pyunits.mol / pyunits.s\n", - ")\n", - "m.fs.ACID.outlet.temperature.fix(298.15 * pyunits.K)\n", - "m.fs.ACID.outlet.pressure.fix(1e5 * pyunits.Pa)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Fixing Unit Model Specifications\n", - "\n", - "Now that we have fixed our inlet feed conditions, we will now be fixing the operating conditions for the unit models in the flowsheet. Let us fix the outlet temperature of H101 to 328.15 K. " - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.H101.outlet.temperature.fix(328.15 * pyunits.K)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We will need to specify both initial reactant extent (conversion or yield) and heat duty values (these are the only two free variables to choose from). The reaction extent can be specified directly, as a molar or mass yield ratio of product to a particular reactant, or fractional conversion of a particular reactant. Here, we choose fractional conversion in terms of ethylene oxide. Since heat duty and the outlet reactor temperature are interdependent, we can choose to specify this quantity instead. While the reaction kinetic parameters exist in the property package, we also do not need to add a rate constant expression since generation is explicitly defined through the conversion/yield. Note that our initial problem will solve with zero *temperature change* but will be infeasible with zero *heat duty*; this is due to the heat of reaction enforced by allowing heat transfer and mandating a non-zero conversion." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.R101.conversion = Var(\n", - " initialize=0.80, bounds=(0, 1), units=pyunits.dimensionless\n", - ") # fraction\n", - "\n", - "m.fs.R101.conv_constraint = Constraint(\n", - " expr=m.fs.R101.conversion\n", - " * m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", - " == (\n", - " m.fs.R101.inlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", - " - m.fs.R101.outlet.flow_mol_phase_comp[0, \"Liq\", \"ethylene_oxide\"]\n", - " )\n", - ")\n", - "\n", - "m.fs.R101.conversion.fix(0.80)\n", - "\n", - "m.fs.R101.outlet.temperature.fix(328.15 * pyunits.K) # equal inlet reactor temperature" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For initialization, we solve a square problem (degrees of freedom = 0). Let's check the degrees of freedom below:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0\n" - ] + ], + "metadata": { + "celltoolbar": "Tags", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.18" } - ], - "source": [ - "print(degrees_of_freedom(m))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Finally, we need to initialize the each unit operation in sequence to solve the flowsheet. As in best practice, unit operations are initialized or solved, and outlet properties are propagated to connected inlet streams via arc definitions as follows:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.OXIDE.properties: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.OXIDE.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.OXIDE.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.OXIDE: Initialization Complete.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.ACID.properties: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.ACID.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.ACID.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.ACID: Initialization Complete.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.M101.reagent_feed_state: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.M101.reagent_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.M101.catalyst_feed_state: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.M101.catalyst_feed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.M101.mixed_state: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.M101.mixed_state: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.M101.mixed_state: Property package initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.M101: Initialization Complete: optimal - Optimal Solution Found\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.H101.control_volume.properties_in: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.H101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.H101.control_volume.properties_out: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.H101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.H101.control_volume: Initialization Complete\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.H101: Initialization Complete: optimal - Optimal Solution Found\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.R101.control_volume.properties_in: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.R101.control_volume.properties_in: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.R101.control_volume.properties_out: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.R101.control_volume.properties_out: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.R101.control_volume.reactions: Initialization Complete.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.R101.control_volume: Initialization Complete\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.R101: Initialization Complete: optimal - Optimal Solution Found\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.PROD.properties: Starting initialization\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.PROD.properties: Property initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.PROD.properties: Property package initialization: optimal - Optimal Solution Found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2023-11-02 10:26:59 [INFO] idaes.init.fs.PROD: Initialization Complete.\n" - ] - } - ], - "source": [ - "# Initialize and solve each unit operation\n", - "m.fs.OXIDE.initialize()\n", - "propagate_state(arc=m.fs.s01)\n", - "\n", - "m.fs.ACID.initialize()\n", - "propagate_state(arc=m.fs.s01)\n", - "\n", - "m.fs.M101.initialize()\n", - "propagate_state(arc=m.fs.s03)\n", - "\n", - "m.fs.H101.initialize()\n", - "propagate_state(arc=m.fs.s04)\n", - "\n", - "m.fs.R101.initialize()\n", - "propagate_state(arc=m.fs.s05)\n", - "\n", - "m.fs.PROD.initialize()\n", - "\n", - "# set solver\n", - "solver = get_solver()" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 337\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 383\n", - "\n", - "Total number of variables............................: 95\n", - " variables with only lower bounds: 0\n", - " variables with lower and upper bounds: 86\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 95\n", - "Total number of inequality constraints...............: 0\n", - " inequality constraints with only lower bounds: 0\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 0.0000000e+00 1.30e+06 0.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 2.66e+06 1.28e+01 -1.0 9.75e+06 - 6.77e-02 9.90e-01h 1\n", - " 2 0.0000000e+00 2.36e+04 2.90e+02 -1.0 9.75e+04 - 7.00e-01 9.90e-01h 1\n", - " 3 0.0000000e+00 2.43e+02 1.44e+01 -1.0 9.74e+02 - 9.90e-01 9.90e-01h 1\n", - " 4 0.0000000e+00 1.85e+00 3.18e+03 -1.0 9.62e+00 - 9.90e-01 9.92e-01h 1\n", - " 5 0.0000000e+00 8.94e-08 3.34e+03 -1.0 7.33e-02 - 9.94e-01 1.00e+00h 1\n", - "Cannot recompute multipliers for feasibility problem. Error in eq_mult_calculator\n", - "\n", - "Number of Iterations....: 5\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 1.6686898422600192e+06 1.6686898422600192e+06\n", - "Constraint violation....: 1.9895196601282805e-13 8.9406967163085938e-08\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 1.9895196601282805e-13 1.6686898422600192e+06\n", - "\n", - "\n", - "Number of objective function evaluations = 6\n", - "Number of objective gradient evaluations = 6\n", - "Number of equality constraint evaluations = 6\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 6\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 5\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "# Solve the model\n", - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Analyze the Results of the Square Problem\n", - "\n", - "\n", - "What is the total operating cost? " - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $3.458 million per year\n" - ] - } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For this operating cost, what conversion did we achieve of ethylene oxide to ethylene glycol? " - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "====================================================================================\n", - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : -5.6566e+06 : watt : False : (None, None)\n", - " Reaction Extent [R1] : 46.400 : mole / second : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Molar Flowrate ('Liq', 'ethylene_oxide') mole / second 58.000 11.600\n", - " Molar Flowrate ('Liq', 'water') mole / second 239.60 193.20\n", - " Molar Flowrate ('Liq', 'sulfuric_acid') mole / second 0.33401 0.33401\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 2.0000e-05 46.400\n", - " Temperature kelvin 328.15 328.15\n", - " Pressure pascal 1.0000e+05 1.0000e+05\n", - "====================================================================================\n", - "\n", - "Conversion achieved = 80.0%\n" - ] - } - ], - "source": [ - "m.fs.R101.report()\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Optimizing Ethylene Glycol Production\n", - "\n", - "Now that the flowsheet has been squared and solved, we can run a small optimization problem to minimize our production costs. Suppose we require at least 200 million pounds/year of ethylene glycol produced and 90% conversion of ethylene oxide, allowing for variable and reactor temperature (heater outlet)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let us declare our objective function for this problem. " - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.objective = Objective(expr=m.fs.operating_cost)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now, we need to add the design constraints and unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now, as well as set bounds for the design variables (reactor outlet temperature is set by state variable bounds in property package):" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.eg_prod_con = Constraint(\n", - " expr=m.fs.eg_prod >= 200 * pyunits.Mlb / pyunits.yr\n", - ") # MM lb/year\n", - "m.fs.R101.conversion.fix(0.90)\n", - "\n", - "m.fs.H101.outlet.temperature.unfix()\n", - "m.fs.H101.outlet.temperature[0].setlb(328.15 * pyunits.K)\n", - "m.fs.H101.outlet.temperature[0].setub(\n", - " 470.45 * pyunits.K\n", - ") # highest component boiling point (ethylene glycol)\n", - "\n", - "m.fs.R101.outlet.temperature.unfix()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "We have now defined the optimization problem and we are now ready to solve this problem. \n", - "\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Ipopt 3.13.2: nlp_scaling_method=gradient-based\n", - "tol=1e-06\n", - "max_iter=200\n", - "\n", - "\n", - "******************************************************************************\n", - "This program contains Ipopt, a library for large-scale nonlinear optimization.\n", - " Ipopt is released as open source code under the Eclipse Public License (EPL).\n", - " For more information visit http://projects.coin-or.org/Ipopt\n", - "\n", - "This version of Ipopt was compiled from source code available at\n", - " https://github.com/IDAES/Ipopt as part of the Institute for the Design of\n", - " Advanced Energy Systems Process Systems Engineering Framework (IDAES PSE\n", - " Framework) Copyright (c) 2018-2019. See https://github.com/IDAES/idaes-pse.\n", - "\n", - "This version of Ipopt was compiled using HSL, a collection of Fortran codes\n", - " for large-scale scientific computation. All technical papers, sales and\n", - " publicity material resulting from use of the HSL codes within IPOPT must\n", - " contain the following acknowledgement:\n", - " HSL, a collection of Fortran codes for large-scale scientific\n", - " computation. See http://www.hsl.rl.ac.uk.\n", - "******************************************************************************\n", - "\n", - "This is Ipopt version 3.13.2, running with linear solver ma27.\n", - "\n", - "Number of nonzeros in equality constraint Jacobian...: 341\n", - "Number of nonzeros in inequality constraint Jacobian.: 1\n", - "Number of nonzeros in Lagrangian Hessian.............: 403\n", - "\n", - "Total number of variables............................: 97\n", - " variables with only lower bounds: 0\n", - " variables with lower and upper bounds: 88\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 95\n", - "Total number of inequality constraints...............: 1\n", - " inequality constraints with only lower bounds: 1\n", - " inequality constraints with lower and upper bounds: 0\n", - " inequality constraints with only upper bounds: 0\n", - "\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 0 3.4581399e+06 1.76e+06 6.34e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 3.4605363e+06 1.75e+06 1.17e+01 -1.0 6.95e+05 - 7.82e-02 6.15e-03h 1\n", - " 2 3.4956495e+06 1.61e+06 6.65e+01 -1.0 6.94e+05 - 1.27e-01 8.29e-02h 1\n", - " 3 3.5669528e+06 1.31e+06 4.78e+02 -1.0 6.33e+05 - 1.61e-01 1.84e-01h 1\n", - " 4 3.6648118e+06 9.11e+05 3.39e+02 -1.0 5.26e+05 - 9.11e-01 3.05e-01h 1\n", - " 5 3.8858215e+06 1.14e+04 1.07e+01 -1.0 3.65e+05 - 9.88e-01 9.90e-01h 1\n", - " 6 3.8880314e+06 1.02e+02 2.00e+00 -1.0 3.65e+03 - 9.90e-01 9.91e-01h 1\n", - " 7 3.8880511e+06 4.13e-05 1.64e-03 -1.0 3.23e+01 - 1.00e+00 1.00e+00h 1\n", - " 8 3.8880508e+06 2.06e-06 1.09e+01 -5.7 3.42e-01 - 1.00e+00 1.00e+00f 1\n", - " 9 3.8880508e+06 2.05e-08 3.69e-07 -5.7 2.15e-05 - 1.00e+00 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10 3.8880508e+06 4.66e-09 4.00e-07 -7.0 6.02e-06 - 1.00e+00 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 10\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 3.8880508414204163e+06 3.8880508414204163e+06\n", - "Dual infeasibility......: 4.0007411785832306e-07 4.0007411785832306e-07\n", - "Constraint violation....: 7.1054273576010019e-15 4.6566128730773926e-09\n", - "Complementarity.........: 9.0909183706024063e-08 9.0909183706024063e-08\n", - "Overall NLP error.......: 9.0909183706024063e-08 4.0007411785832306e-07\n", - "\n", - "\n", - "Number of objective function evaluations = 11\n", - "Number of objective gradient evaluations = 11\n", - "Number of equality constraint evaluations = 11\n", - "Number of inequality constraint evaluations = 11\n", - "Number of equality constraint Jacobian evaluations = 11\n", - "Number of inequality constraint Jacobian evaluations = 11\n", - "Number of Lagrangian Hessian evaluations = 10\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", - "Total CPU secs in NLP function evaluations = 0.001\n", - "\n", - "EXIT: Optimal Solution Found.\n" - ] - } - ], - "source": [ - "results = solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "operating cost = $3.888 million per year\n", - "\n", - "Heater results\n", - "\n", - "====================================================================================\n", - "Unit : fs.H101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : 699.26 : watt : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Molar Flowrate ('Liq', 'ethylene_oxide') mole / second 58.000 58.000\n", - " Molar Flowrate ('Liq', 'water') mole / second 239.60 239.60\n", - " Molar Flowrate ('Liq', 'sulfuric_acid') mole / second 0.33401 0.33401\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 2.0000e-05 2.0000e-05\n", - " Temperature kelvin 298.15 328.15\n", - " Pressure pascal 1.0000e+05 1.0000e+05\n", - "====================================================================================\n", - "\n", - "Stoichiometric reactor results\n", - "\n", - "====================================================================================\n", - "Unit : fs.R101 Time: 0.0\n", - "------------------------------------------------------------------------------------\n", - " Unit Performance\n", - "\n", - " Variables: \n", - "\n", - " Key : Value : Units : Fixed : Bounds\n", - " Heat Duty : -6.3608e+06 : watt : False : (None, None)\n", - " Reaction Extent [R1] : 52.200 : mole / second : False : (None, None)\n", - "\n", - "------------------------------------------------------------------------------------\n", - " Stream Table\n", - " Units Inlet Outlet \n", - " Molar Flowrate ('Liq', 'ethylene_oxide') mole / second 58.000 5.8000\n", - " Molar Flowrate ('Liq', 'water') mole / second 239.60 187.40\n", - " Molar Flowrate ('Liq', 'sulfuric_acid') mole / second 0.33401 0.33401\n", - " Molar Flowrate ('Liq', 'ethylene_glycol') mole / second 2.0000e-05 52.200\n", - " Temperature kelvin 328.15 450.00\n", - " Pressure pascal 1.0000e+05 1.0000e+05\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", - "\n", - "print()\n", - "print(\"Heater results\")\n", - "\n", - "m.fs.H101.report()\n", - "\n", - "print()\n", - "print(\"Stoichiometric reactor results\")\n", - "\n", - "m.fs.R101.report()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Display optimal values for the decision variables and design variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Optimal Values\n", - "\n", - "H101 outlet temperature = 328.150 K\n", - "\n", - "R101 outlet temperature = 450.000 K\n", - "\n", - "Ethylene glycol produced = 225.415 MM lb/year\n", - "\n", - "Conversion achieved = 90.0%\n" - ] - } - ], - "source": [ - "print(\"Optimal Values\")\n", - "print()\n", - "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", - "\n", - "print()\n", - "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.3f} K\")\n", - "\n", - "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", - "\n", - "print()\n", - "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "celltoolbar": "Tags", - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 3 -} + "nbformat": 4, + "nbformat_minor": 3 +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_test.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_test.ipynb index 6ae99dc8..675c7fea 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_test.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_test.ipynb @@ -53,7 +53,7 @@ "\n", "Chemical reaction:\n", "\n", - "**C2H4O + H2O + H2SO4 → C2H6O2 + H2SO4**\n", + "**C2H4O + H2O + H2SO4 \u2192 C2H6O2 + H2SO4**\n", "\n", "Property Packages:\n", "\n", @@ -219,9 +219,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = StoichiometricReactor(\n", @@ -564,7 +562,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" ] }, { @@ -579,7 +577,7 @@ "source": [ "import pytest\n", "\n", - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(3.458140, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(8.019605, rel=1e-5)" ] }, { @@ -611,9 +609,9 @@ }, "outputs": [], "source": [ - "assert value(m.fs.R101.conversion) == pytest.approx(0.8000, abs=1e-3)\n", - "assert value(m.fs.R101.heat_duty[0]) / 1e6 == pytest.approx(-5.6566, abs=1e-3)\n", - "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2815, abs=1e-3)" + "assert value(m.fs.R101.conversion) == pytest.approx(0.8000, rel=1e-5)\n", + "assert value(m.fs.R101.heat_duty[0]) / 1e6 == pytest.approx(-5.8931, rel=1e-5)\n", + "assert value(m.fs.R101.outlet.temperature[0]) / 1e2 == pytest.approx(3.2815, rel=1e-5)" ] }, { @@ -724,7 +722,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", "\n", "print()\n", "print(\"Heater results\")\n", @@ -747,7 +745,7 @@ }, "outputs": [], "source": [ - "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(3.888050, abs=1e-3)" + "assert value(m.fs.operating_cost) / 1e6 == pytest.approx(6.670729, rel=1e-5)" ] }, { @@ -766,13 +764,13 @@ "print(\"Optimal Values\")\n", "print()\n", "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", - "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", "\n", "print()\n", "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" @@ -788,10 +786,10 @@ }, "outputs": [], "source": [ - "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, abs=1e-3)\n", - "assert value(m.fs.R101.outlet.temperature[0]) / 100 == pytest.approx(4.5000, abs=1e-3)\n", - "assert value(m.fs.eg_prod) == pytest.approx(225.415, abs=1e-3)\n", - "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.0, abs=1e-3)" + "assert value(m.fs.H101.outlet.temperature[0]) / 100 == pytest.approx(3.2815, rel=1e-5)\n", + "assert value(m.fs.R101.outlet.temperature[0]) / 100 == pytest.approx(4.5000, rel=1e-5)\n", + "assert value(m.fs.eg_prod) == pytest.approx(225.415, rel=1e-5)\n", + "assert value(m.fs.R101.conversion) * 100 == pytest.approx(90.0, rel=1e-5)" ] }, { @@ -819,9 +817,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_usr.ipynb b/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_usr.ipynb index 06425641..5660e55a 100644 --- a/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_usr.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/reactors/stoichiometric_reactor_usr.ipynb @@ -53,7 +53,7 @@ "\n", "Chemical reaction:\n", "\n", - "**C2H4O + H2O + H2SO4 → C2H6O2 + H2SO4**\n", + "**C2H4O + H2O + H2SO4 \u2192 C2H6O2 + H2SO4**\n", "\n", "Property Packages:\n", "\n", @@ -219,9 +219,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "scrolled": false - }, + "metadata": {}, "outputs": [], "source": [ "m.fs.R101 = StoichiometricReactor(\n", @@ -520,7 +518,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")" + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")" ] }, { @@ -621,7 +619,7 @@ "metadata": {}, "outputs": [], "source": [ - "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.3f} million per year\")\n", + "print(f\"operating cost = ${value(m.fs.operating_cost)/1e6:0.6f} million per year\")\n", "\n", "print()\n", "print(\"Heater results\")\n", @@ -650,13 +648,13 @@ "print(\"Optimal Values\")\n", "print()\n", "\n", - "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", - "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.3f} K\")\n", + "print(f\"R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):0.6f} K\")\n", "\n", "print()\n", - "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.3f} MM lb/year\")\n", + "print(f\"Ethylene glycol produced = {value(m.fs.eg_prod):0.6f} MM lb/year\")\n", "\n", "print()\n", "print(f\"Conversion achieved = {value(m.fs.R101.conversion):.1%}\")" @@ -687,9 +685,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.12" + "version": "3.9.18" } }, "nbformat": 4, "nbformat_minor": 3 -} +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/tests/__init__.py b/idaes_examples/notebooks/docs/unit_models/reactors/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/idaes_examples/notebooks/docs/unit_models/reactors/tests/test_egprod_ideal.py b/idaes_examples/notebooks/docs/unit_models/reactors/tests/test_egprod_ideal.py new file mode 100644 index 00000000..7b9ca054 --- /dev/null +++ b/idaes_examples/notebooks/docs/unit_models/reactors/tests/test_egprod_ideal.py @@ -0,0 +1,1074 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# +""" +Author: Brandon Paul +""" +import pytest +from pyomo.environ import ( + assert_optimal_termination, + ConcreteModel, + Set, + value, + Var, + units as pyunits, + as_quantity, +) +from pyomo.common.unittest import assertStructuredAlmostEqual + +from idaes.core import Component +from idaes.core.util.model_statistics import ( + degrees_of_freedom, + fixed_variables_set, + activated_constraints_set, +) +from idaes.core.solvers import get_solver + +from idaes.models.properties.modular_properties.base.generic_property import ( + GenericParameterBlock, +) + +from idaes.models.properties.modular_properties.state_definitions import FpcTP + +from idaes_examples.notebooks.docs.unit_models.reactors.egprod_ideal import config_dict + +from idaes.models.properties.tests.test_harness import PropertyTestHarness + +from idaes.core.util.model_diagnostics import DiagnosticsToolbox + +from idaes.core.util.constants import Constants as const + +from idaes.core import VaporPhase + +from idaes.models.properties.modular_properties.eos.ideal import Ideal + +import copy + + +# ----------------------------------------------------------------------------- +# Get default solver for testing +solver = get_solver() + + +class TestEGProdIdeal(PropertyTestHarness): + def configure(self): + self.prop_pack = GenericParameterBlock + self.param_args = config_dict + self.prop_args = {} + self.has_density_terms = True + + +class TestParamBlock(object): + @pytest.mark.unit + def test_build(self): + model = ConcreteModel() + model.params = GenericParameterBlock(**config_dict) + + assert isinstance(model.params.phase_list, Set) + assert len(model.params.phase_list) == 1 + for i in model.params.phase_list: + assert i in [ + "Liq", + ] + assert model.params.Liq.is_liquid_phase() + + assert isinstance(model.params.component_list, Set) + assert len(model.params.component_list) == 4 + for i in model.params.component_list: + assert i in ["ethylene_oxide", "water", "sulfuric_acid", "ethylene_glycol"] + assert isinstance(model.params.get_component(i), Component) + + assert isinstance(model.params._phase_component_set, Set) + assert len(model.params._phase_component_set) == 4 + for i in model.params._phase_component_set: + assert i in [ + ("Liq", "ethylene_oxide"), + ("Liq", "water"), + ("Liq", "sulfuric_acid"), + ("Liq", "ethylene_glycol"), + ] + + assert model.params.config.state_definition == FpcTP + + assertStructuredAlmostEqual( + model.params.config.state_bounds, + { + "flow_mol_phase_comp": (0, 100, 1000, pyunits.mol / pyunits.s), + "temperature": (273.15, 298.15, 450, pyunits.K), + "pressure": (5e4, 1e5, 1e6, pyunits.Pa), + }, + item_callback=as_quantity, + ) + + assert value(model.params.pressure_ref) == 1e5 + assert value(model.params.temperature_ref) == 298.15 + + assert value(model.params.ethylene_oxide.mw) == 44.054e-3 + assert value(model.params.ethylene_oxide.pressure_crit) == 71.9e5 + assert value(model.params.ethylene_oxide.temperature_crit) == 469 + + assert value(model.params.water.mw) == 18.015e-3 + assert value(model.params.water.pressure_crit) == 221.2e5 + assert value(model.params.water.temperature_crit) == 647.3 + + assert value(model.params.sulfuric_acid.mw) == 98.08e-3 + assert value(model.params.sulfuric_acid.pressure_crit) == 129.4262e5 + assert value(model.params.sulfuric_acid.temperature_crit) == 590.76 + + assert value(model.params.ethylene_glycol.mw) == 62.069e-3 + assert value(model.params.ethylene_glycol.pressure_crit) == 77e5 + assert value(model.params.ethylene_glycol.temperature_crit) == 645 + + dt = DiagnosticsToolbox(model) + dt.assert_no_structural_warnings() + + +class TestStateBlock(object): + @pytest.fixture(scope="class") + def model(self): + model = ConcreteModel() + model.params = GenericParameterBlock(**config_dict) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Liq", "ethylene_oxide"].fix(100) + model.props[1].flow_mol_phase_comp["Liq", "water"].fix(100) + model.props[1].flow_mol_phase_comp["Liq", "sulfuric_acid"].fix(100) + model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"].fix(100) + model.props[1].temperature.fix(300) + model.props[1].pressure.fix(101325) + + return model + + @pytest.mark.unit + def test_build(self, model): + # Check state variable values and bounds + assert isinstance(model.props[1].flow_mol_phase_comp, Var) + assert value(model.props[1].flow_mol_phase_comp["Liq", "ethylene_oxide"]) == 100 + assert value(model.props[1].flow_mol_phase_comp["Liq", "water"]) == 100 + assert value(model.props[1].flow_mol_phase_comp["Liq", "sulfuric_acid"]) == 100 + assert ( + value(model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"]) == 100 + ) + assert model.props[1].flow_mol_phase_comp["Liq", "ethylene_oxide"].ub == 1000 + assert model.props[1].flow_mol_phase_comp["Liq", "water"].ub == 1000 + assert model.props[1].flow_mol_phase_comp["Liq", "sulfuric_acid"].ub == 1000 + assert model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"].ub == 1000 + assert model.props[1].flow_mol_phase_comp["Liq", "ethylene_oxide"].lb == 0 + assert model.props[1].flow_mol_phase_comp["Liq", "water"].lb == 0 + assert model.props[1].flow_mol_phase_comp["Liq", "sulfuric_acid"].lb == 0 + assert model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"].lb == 0 + + assert isinstance(model.props[1].pressure, Var) + assert value(model.props[1].pressure) == 101325 + assert model.props[1].pressure.ub == 1e6 + assert model.props[1].pressure.lb == 5e4 + + assert isinstance(model.props[1].temperature, Var) + assert value(model.props[1].temperature) == 300 + assert model.props[1].temperature.ub == 450 + assert model.props[1].temperature.lb == 273.15 + + @pytest.mark.unit + def test_define_state_vars(self, model): + sv = model.props[1].define_state_vars() + + assert len(sv) == 3 + for i in sv: + assert i in ["flow_mol_phase_comp", "temperature", "pressure"] + + @pytest.mark.unit + def test_define_port_members(self, model): + sv = model.props[1].define_state_vars() + + assert len(sv) == 3 + for i in sv: + assert i in ["flow_mol_phase_comp", "temperature", "pressure"] + + @pytest.mark.unit + def test_define_display_vars(self, model): + sv = model.props[1].define_display_vars() + + assert len(sv) == 3 + for i in sv: + assert i in [ + "Molar Flowrate", + "Temperature", + "Pressure", + ] + + @pytest.mark.unit + def test_structural_diagnostics(self, model): + dt = DiagnosticsToolbox(model) + dt.assert_no_structural_warnings() + + @pytest.mark.unit + def test_basic_scaling(self, model): + assert len(model.props[1].scaling_factor) == 20 + assert model.props[1].scaling_factor[model.props[1].flow_mol] == 1e-2 + assert ( + model.props[1].scaling_factor[ + model.props[1].flow_mol_comp["ethylene_oxide"] + ] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[model.props[1].flow_mol_comp["water"]] == 1e-2 + ) + assert ( + model.props[1].scaling_factor[model.props[1].flow_mol_comp["sulfuric_acid"]] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].flow_mol_comp["ethylene_glycol"] + ] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[model.props[1].flow_mol_phase["Liq"]] == 1e-2 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].flow_mol_phase_comp["Liq", "ethylene_oxide"] + ] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].flow_mol_phase_comp["Liq", "water"] + ] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].flow_mol_phase_comp["Liq", "sulfuric_acid"] + ] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"] + ] + == 1e-2 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_comp["ethylene_oxide"] + ] + == 1000 + ) + assert ( + model.props[1].scaling_factor[model.props[1].mole_frac_comp["water"]] + == 1000 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_comp["sulfuric_acid"] + ] + == 1000 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_comp["ethylene_glycol"] + ] + == 1000 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_phase_comp["Liq", "ethylene_oxide"] + ] + == 1000 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_phase_comp["Liq", "water"] + ] + == 1000 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_phase_comp["Liq", "sulfuric_acid"] + ] + == 1000 + ) + assert ( + model.props[1].scaling_factor[ + model.props[1].mole_frac_phase_comp["Liq", "ethylene_glycol"] + ] + == 1000 + ) + assert model.props[1].scaling_factor[model.props[1].pressure] == 1e-5 + assert model.props[1].scaling_factor[model.props[1].temperature] == 1e-2 + + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_initialize(self, model): + orig_fixed_vars = fixed_variables_set(model) + orig_act_consts = activated_constraints_set(model) + + model.props.initialize(optarg={"tol": 1e-6}) + + assert degrees_of_freedom(model) == 0 + + fin_fixed_vars = fixed_variables_set(model) + fin_act_consts = activated_constraints_set(model) + + assert len(fin_act_consts) == len(orig_act_consts) + assert len(fin_fixed_vars) == len(orig_fixed_vars) + + for c in fin_act_consts: + assert c in orig_act_consts + for v in fin_fixed_vars: + assert v in orig_fixed_vars + + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solve(self, model): + results = solver.solve(model) + + # Check for optimal solution + assert_optimal_termination(results) + + @pytest.mark.unit + def test_numerical_diagnostics(self, model): + dt = DiagnosticsToolbox(model) + dt.assert_no_numerical_warnings() + + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_solution(self, model): + # Check results + assert value( + model.props[1].flow_mol_phase_comp["Liq", "ethylene_oxide"] + ) == pytest.approx(100, abs=1e-4) + assert value( + model.props[1].flow_mol_phase_comp["Liq", "water"] + ) == pytest.approx(100, abs=1e-4) + assert value( + model.props[1].flow_mol_phase_comp["Liq", "sulfuric_acid"] + ) == pytest.approx(100, abs=1e-4) + assert value( + model.props[1].flow_mol_phase_comp["Liq", "ethylene_glycol"] + ) == pytest.approx(100, abs=1e-4) + + assert value(model.props[1].temperature) == pytest.approx(300, abs=1e-4) + assert value(model.props[1].pressure) == pytest.approx(101325, abs=1e-4) + + +class TestPerrysProperties(object): + @pytest.fixture(scope="class") + def density_temperatures(self): + # ethylene oxide, water, ethylene glycol reference temperatures + # from Perry's Chemical Engineers' Handbook 7th Ed. 2-94 to 2-98 + components = ["ethylene_oxide", "water", "ethylene_glycol"] + temperatures = dict( + zip(components, [[160.65, 469.15], [273.16, 333.15], [260.15, 719.7]]) + ) + + return temperatures + + @pytest.fixture(scope="class") + def densities(self): + # ethylene oxide, water, ethylene glycol densities from + # Perry's Chemical Engineers' Handbook 7th Ed. 2-94 to 2-98 + components = ["ethylene_oxide", "water", "ethylene_glycol"] + densities = dict( + zip(components, [[23.477, 7.055], [55.583, 54.703], [18.31, 5.234]]) + ) + + return densities + + @pytest.fixture(scope="class") + def heat_capacity_temperatures(self): + # ethylene oxide, water, ethylene glycol reference temperatures + # from Perry's Chemical Engineers' Handbook 7th Ed. 2-170 to 2-174 + components = ["ethylene_oxide", "water", "ethylene_glycol"] + temperatures = dict( + zip(components, [[160.65, 283.85], [273.16, 533.15], [260.15, 493.15]]) + ) + + return temperatures + + @pytest.fixture(scope="class") + def heat_capacities(self): + # ethylene oxide, water, ethylene glycol heat capacities from + # Perry's Chemical Engineers' Handbook 7th Ed. 2-170 to 2-174 + components = ["ethylene_oxide", "water", "ethylene_glycol"] + heat_capacities = dict( + zip( + components, + [[0.8303e5, 0.8693e5], [0.7615e5, 0.8939e5], [1.36661e5, 2.0598e5]], + ) + ) + + return heat_capacities + + @pytest.fixture(scope="class") + def heat_capacity_reference(self): + # ethylene oxide, water, ethylene glycol heat capacities from + # NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["ethylene_oxide", "water", "ethylene_glycol"] + heat_capacities = dict(zip(components, [0.8690e5, 0.7538e5, 0.1498e5])) + + return heat_capacities + + @pytest.fixture(scope="class") + def heat_capacity_reference_temperatures(self): + # ethylene oxide, water, ethylene glycol reference temperatures + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["ethylene_oxide", "water", "ethylene_glycol"] + temperatures = dict(zip(components, [285, 298.0, 298.0])) + + return temperatures + + @pytest.mark.parametrize( + "component", ["ethylene_oxide", "water", "ethylene_glycol"] + ) + @pytest.mark.parametrize("test_point", [0, 1]) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_liquid_densities( + self, component, test_point, density_temperatures, densities + ): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == component: + pass + else: + config_dict_component_only["components"].pop(key) + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Liq", component].fix(100) + + # change lower bound for testing + model.props[1].temperature.setlb(150) + + model.props[1].temperature.fix(density_temperatures[component][test_point]) + model.props[1].pressure.fix(101325) + + results = solver.solve(model) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + assert value( + pyunits.convert( + model.props[1].dens_mol, to_units=pyunits.kmol / pyunits.m**3 + ) + ) == pytest.approx(densities[component][test_point], rel=1e-4) + + @pytest.mark.parametrize( + "component", ["ethylene_oxide", "water", "ethylene_glycol"] + ) + @pytest.mark.parametrize("test_point", [0, 1]) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_liquid_heat_capacities_enthalpy( + self, + component, + test_point, + heat_capacity_temperatures, + heat_capacities, + heat_capacity_reference, + heat_capacity_reference_temperatures, + ): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == component: + pass + else: + config_dict_component_only["components"].pop(key) + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Liq", component].fix(100) + + model.props[1].pressure.fix(101325) + + # calculate reference point + + model.props[1].temperature.fix(heat_capacity_reference_temperatures[component]) + + results = solver.solve(model) + + enth_mol_ref = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_ref = heat_capacity_reference_temperatures[component] * pyunits.K + cp_mol_ref = ( + heat_capacity_reference[component] + * 1e-3 + * pyunits.J + / pyunits.mol + / pyunits.K + ) + + # calculate test point + + model.props[1].temperature.fix( + heat_capacity_temperatures[component][test_point] + ) + + results = solver.solve(model) + + enth_mol_test = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_test = heat_capacity_temperatures[component][test_point] * pyunits.K + cp_mol_test = ( + heat_capacities[component][test_point] + * 1e-3 + * pyunits.J + / pyunits.mol + / pyunits.K + ) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + + assert value( + pyunits.convert(enth_mol_test, to_units=pyunits.J / pyunits.mol) + ) == pytest.approx( + value( + pyunits.convert( + 0.5 * (cp_mol_test + cp_mol_ref) * (temp_test - temp_ref) + + enth_mol_ref, + to_units=pyunits.J / pyunits.mol, + ) + ), + rel=1e-1, # using 1e-1 tol to check against trapezoid rule estimation of integral + ) + + +class TestRPP4Properties(object): + @pytest.fixture(scope="class") + def heat_capacity_temperatures(self): + # ethylene oxide, water, ethylene glycol reference temperatures + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["ethylene_oxide", "water", "ethylene_glycol"] + temperatures = dict(zip(components, [[307.18, 371.23], [545, 632], [500, 600]])) + + return temperatures + + @pytest.fixture(scope="class") + def heat_capacities(self): + # ethylene oxide, water, ethylene glycol heat capacities from + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["ethylene_oxide", "water", "ethylene_glycol"] + heat_capacities = dict( + zip(components, [[49.37, 58.41], [35.70, 36.69], [113.64, 125.65]]) + ) + + return heat_capacities + + @pytest.fixture(scope="class") + def heat_capacity_reference(self): + # ethylene oxide, water, ethylene glycol heat capacities from + # NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["ethylene_oxide", "water", "ethylene_glycol"] + heat_capacities = dict(zip(components, [61.66, 35.22, 97.99])) + + return heat_capacities + + @pytest.fixture(scope="class") + def heat_capacity_reference_temperatures(self): + # ethylene oxide, water, ethylene glycol reference temperatures + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["ethylene_oxide", "water", "ethylene_glycol"] + temperatures = dict(zip(components, [400, 500, 400])) + + return temperatures + + @pytest.fixture(scope="class") + def saturation_pressure_temperatures(self): + # ethylene oxide, water, ethylene glycol reference temperatures + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["ethylene_oxide", "water", "ethylene_glycol"] + temperatures = dict( + zip(components, [[250.01, 300.02], [300.25, 350.16], [387, 473]]) + ) + + return temperatures + + @pytest.fixture(scope="class") + def saturation_pressures(self): + # ethylene oxide, water, ethylene glycol saturation pressures from + # from NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + components = ["ethylene_oxide", "water", "ethylene_glycol"] + pressures = dict( + zip( + components, + [[0.2189e5, 1.8604e5], [0.03591e5, 0.4194e5], [0.04257e5, 1.0934e5]], + ) + ) + + return pressures + + @pytest.mark.parametrize( + "component", ["ethylene_oxide", "water", "ethylene_glycol"] + ) + @pytest.mark.parametrize("test_point", [0, 1]) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_vapor_heat_capacities_enthalpy( + self, + component, + test_point, + heat_capacity_temperatures, + heat_capacities, + heat_capacity_reference, + heat_capacity_reference_temperatures, + ): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == component: + pass + else: + config_dict_component_only["components"].pop(key) + + config_dict_component_only["phases"] = { + "Vap": {"type": VaporPhase, "equation_of_state": Ideal} + } + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Vap", component].fix(100) + + model.props[1].pressure.fix(101325) + + # calculate reference point + + model.props[1].temperature.fix(heat_capacity_reference_temperatures[component]) + + results = solver.solve(model) + + enth_mol_ref = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_ref = heat_capacity_reference_temperatures[component] * pyunits.K + cp_mol_ref = ( + heat_capacity_reference[component] + * 1e-3 + * pyunits.J + / pyunits.mol + / pyunits.K + ) + + # calculate test point + + model.props[1].temperature.fix( + heat_capacity_temperatures[component][test_point] + ) + + results = solver.solve(model) + + enth_mol_test = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_test = heat_capacity_temperatures[component][test_point] * pyunits.K + cp_mol_test = ( + heat_capacities[component][test_point] + * 1e-3 + * pyunits.J + / pyunits.mol + / pyunits.K + ) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + + assert value( + pyunits.convert(enth_mol_test, to_units=pyunits.J / pyunits.mol) + ) == pytest.approx( + value( + pyunits.convert( + 0.5 * (cp_mol_test + cp_mol_ref) * (temp_test - temp_ref) + + enth_mol_ref, + to_units=pyunits.J / pyunits.mol, + ) + ), + rel=1.15e-1, # using 1.15e-1 tol to check against trapezoid rule estimation of integral + # all values match within 1e-1, except ethylene glycol test point 0 + ) + + @pytest.mark.parametrize( + "component", ["ethylene_oxide", "water", "ethylene_glycol"] + ) + @pytest.mark.parametrize("test_point", [0, 1]) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_saturation_pressures( + self, + component, + test_point, + saturation_pressure_temperatures, + saturation_pressures, + ): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == component: + pass + else: + config_dict_component_only["components"].pop(key) + + config_dict_component_only["phases"] = { + "Vap": {"type": VaporPhase, "equation_of_state": Ideal} + } + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Vap", component].fix(100) + + model.props[1].temperature.fix( + saturation_pressure_temperatures[component][test_point] + ) + model.props[1].pressure.fix(101325) + + results = solver.solve(model) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + print(value(model.props[1].pressure_sat_comp[component])) + assert value(model.props[1].pressure_sat_comp[component]) == pytest.approx( + saturation_pressures[component][test_point], rel=1.5e-2 + ) # match within 1.5% + + +class TestSulfuricAcidProperties(object): + # sulfuric acid liquid density data from + # CRC Handbook of Chemistry and Physics, 97th Ed., W.M. Haynes pg. 15-41 + @pytest.mark.parametrize( + "temperature_density_data", + [ + [273.15, 1.8517], # K, g/mL + [283.15, 1.8409], + [288.15, 1.8357], + [293.15, 1.8305], + [298.15, 1.8255], + [303.15, 1.8205], + [313.15, 1.8107], + [323.15, 1.8013], + [333.15, 1.7922], + ], + ) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_liquid_densities(self, temperature_density_data): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == "sulfuric_acid": + pass + else: + config_dict_component_only["components"].pop(key) + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Liq", "sulfuric_acid"].fix(100) + + # change lower bound for testing + model.props[1].temperature.setlb(150) + + model.props[1].temperature.fix(temperature_density_data[0]) + model.props[1].pressure.fix(101325) + + results = solver.solve(model) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + assert value( + pyunits.convert( + model.props[1].dens_mol * model.props[1].params.sulfuric_acid.mw, + to_units=pyunits.g / pyunits.mL, + ) + ) == pytest.approx(temperature_density_data[1], rel=1e-4) + + # sulfuric acid liquid heat capacity data from + # Journal of Physical and Chemical Reference Data 20, 1157 (1991); https:// doi.org/10.1063/1.555899 + @pytest.mark.parametrize( + "temperature_liquid_heat_capacity_data", + [ + [250, 15.1606], # K, Cp/R + [287.93, 16.4195], + [293.14, 16.4653], + [298.15, 16.6818], + [300, 16.7319], + [305.35, 16.8788], + [350, 17.8491], + ], + ) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_liquid_heat_capacities_enthalpy( + self, temperature_liquid_heat_capacity_data + ): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == "sulfuric_acid": + pass + else: + config_dict_component_only["components"].pop(key) + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Liq", "sulfuric_acid"].fix(100) + + model.props[1].pressure.fix(101325) + + # calculate reference point + + model.props[1].temperature.fix(200) + + results = solver.solve(model) + + enth_mol_ref = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_ref = 200 * pyunits.K + cp_mol_ref = 13.1352 * const.gas_constant + + # calculate test point + + model.props[1].temperature.fix(temperature_liquid_heat_capacity_data[0]) + + results = solver.solve(model) + + enth_mol_test = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_test = temperature_liquid_heat_capacity_data[0] * pyunits.K + cp_mol_test = temperature_liquid_heat_capacity_data[1] * const.gas_constant + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + + assert value( + pyunits.convert(enth_mol_test, to_units=pyunits.J / pyunits.mol) + ) == pytest.approx( + value( + pyunits.convert( + 0.5 * (cp_mol_test + cp_mol_ref) * (temp_test - temp_ref) + + enth_mol_ref, + to_units=pyunits.J / pyunits.mol, + ) + ), + rel=1e-1, # using 1e-1 tol to check against trapezoid rule estimation of integral + ) + + # sulfuric acid vapor heat capacity data from + # NIST Chemistry WebBook, https://webbook.nist.gov/chemistry/ + @pytest.mark.parametrize( + "temperature_vapor_heat_capacity_data", + [ + [300, 84.02], # K, J/mol-K + [400, 97.90], + [500, 107.9], + [600, 115.6], + [700, 121.5], + [800, 126.1], + [900, 129.7], + [1000, 132.6], + [1100, 135.2], + [1200, 137.2], + ], + ) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_vapor_heat_capacities_enthalpy(self, temperature_vapor_heat_capacity_data): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == "sulfuric_acid": + pass + else: + config_dict_component_only["components"].pop(key) + + config_dict_component_only["phases"] = { + "Vap": {"type": VaporPhase, "equation_of_state": Ideal} + } + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Vap", "sulfuric_acid"].fix(100) + + model.props[1].pressure.fix(101325) + + # calculate reference point + + model.props[1].temperature.fix(298) + + results = solver.solve(model) + + enth_mol_ref = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_ref = 298 * pyunits.K + cp_mol_ref = 83.68 * pyunits.J / pyunits.mol / pyunits.K + + # calculate test point + + model.props[1].temperature.fix(temperature_vapor_heat_capacity_data[0]) + + results = solver.solve(model) + + enth_mol_test = value(model.props[1].enth_mol) * pyunits.get_units( + model.props[1].enth_mol + ) + temp_test = temperature_vapor_heat_capacity_data[0] * pyunits.K + cp_mol_test = ( + temperature_vapor_heat_capacity_data[1] + * pyunits.J + / pyunits.mol + / pyunits.K + ) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + + assert value( + pyunits.convert(enth_mol_test, to_units=pyunits.J / pyunits.mol) + ) == pytest.approx( + value( + pyunits.convert( + 0.5 * (cp_mol_test + cp_mol_ref) * (temp_test - temp_ref) + + enth_mol_ref, + to_units=pyunits.J / pyunits.mol, + ) + ), + rel=1e-1, # using 1e-1 tol to check against trapezoid rule estimation of integral + ) + + # sulfuric acid saturation pressure data from + # CRC Handbook of Chemistry and Physics, 97th Ed., W.M. Haynes pg. 6-122 + @pytest.mark.parametrize( + "temperature_saturation_pressure_data", + [ + [295.3722222, 1], # K, Pa + [312.5944444, 10], + [333.15, 100], + [359.2611111, 1000], + [393.15, 10000], + [438.7055556, 100000], + ], + ) + @pytest.mark.skipif(solver is None, reason="Solver not available") + @pytest.mark.component + def test_saturation_pressures(self, temperature_saturation_pressure_data): + + config_dict_component_only = copy.deepcopy(config_dict) + for key in config_dict["components"].keys(): + if key == "sulfuric_acid": + pass + else: + config_dict_component_only["components"].pop(key) + + config_dict_component_only["phases"] = { + "Vap": {"type": VaporPhase, "equation_of_state": Ideal} + } + + model = ConcreteModel() + + model.params = GenericParameterBlock(**config_dict_component_only) + + model.props = model.params.build_state_block([1], defined_state=True) + + model.props[1].calculate_scaling_factors() + + # Fix state + model.props[1].flow_mol_phase_comp["Vap", "sulfuric_acid"].fix(100) + + model.props[1].temperature.fix(temperature_saturation_pressure_data[0]) + model.props[1].pressure.fix(101325) + + results = solver.solve(model) + + # Check for optimal solution + assert_optimal_termination(results) + + # Check results + assert value( + model.props[1].pressure_sat_comp["sulfuric_acid"] + ) == pytest.approx( + temperature_saturation_pressure_data[1], rel=1.5e-2 + ) # match within 1.5%