\n",
"
Inline Exercise:\n",
"Fill in the missing code below and add a constraint on the mole fraction of Benzene (to a value of 0.6) to find the required heat duty.\n",
@@ -816,14 +1777,16 @@
]
},
{
- "cell_type": "code",
- "execution_count": null,
"metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-06-06T17:05:05.363302Z",
+ "start_time": "2025-06-06T17:05:04.960601Z"
+ },
"tags": [
"exercise"
]
},
- "outputs": [],
+ "cell_type": "code",
"source": [
"# re-initialize the model - this may or may not be required depending on current state but safe to initialize\n",
"m.fs.flash.heat_duty.fix(0)\n",
@@ -839,17 +1802,118 @@
"\n",
"# Check stream condition\n",
"m.fs.flash.report()"
- ]
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "WARNING: model contains export suffix 'scaling_factor' that contains 6\n",
+ "component keys that are not exported as part of the NL file. Skipping.\n",
+ "Ipopt 3.13.2: \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...: 136\n",
+ "Number of nonzeros in inequality constraint Jacobian.: 0\n",
+ "Number of nonzeros in Lagrangian Hessian.............: 72\n",
+ "\n",
+ "Total number of variables............................: 42\n",
+ " variables with only lower bounds: 3\n",
+ " variables with lower and upper bounds: 10\n",
+ " variables with only upper bounds: 0\n",
+ "Total number of equality constraints.................: 41\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 3.07e-08 1.01e-04 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n",
+ " 1 0.0000000e+00 1.46e+00 2.42e-01 -1.0 4.86e+02 - 9.90e-01 1.00e+00f 1\n",
+ " 2 0.0000000e+00 5.55e+01 5.60e-03 -1.0 3.00e+03 - 9.90e-01 1.00e+00h 1\n",
+ " 3 0.0000000e+00 1.91e+00 4.24e-05 -1.0 5.68e+02 - 1.00e+00 1.00e+00h 1\n",
+ " 4 0.0000000e+00 1.53e-05 1.90e-06 -2.5 1.35e+00 - 1.00e+00 1.00e+00h 1\n",
+ " 5 0.0000000e+00 4.66e-10 1.50e-09 -3.8 8.72e-03 - 1.00e+00 1.00e+00h 1\n",
+ " 6 0.0000000e+00 2.18e-11 1.84e-11 -5.7 1.92e-03 - 1.00e+00 1.00e+00h 1\n",
+ " 7 0.0000000e+00 1.46e-11 2.51e-14 -8.6 2.10e-04 - 1.00e+00 1.00e+00H 1\n",
+ "\n",
+ "Number of Iterations....: 7\n",
+ "\n",
+ " (scaled) (unscaled)\n",
+ "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n",
+ "Dual infeasibility......: 2.5059035640133008e-14 2.5059035640133008e-14\n",
+ "Constraint violation....: 4.8801876740451558e-13 1.4551915228366852e-11\n",
+ "Complementarity.........: 2.5059101787473095e-09 2.5059101787473095e-09\n",
+ "Overall NLP error.......: 2.5059101787473095e-09 2.5059101787473095e-09\n",
+ "\n",
+ "\n",
+ "Number of objective function evaluations = 9\n",
+ "Number of objective gradient evaluations = 8\n",
+ "Number of equality constraint evaluations = 9\n",
+ "Number of inequality constraint evaluations = 0\n",
+ "Number of equality constraint Jacobian evaluations = 8\n",
+ "Number of inequality constraint Jacobian evaluations = 0\n",
+ "Number of Lagrangian Hessian evaluations = 7\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",
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.flash Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : 4059.3 : watt : False : (None, None)\n",
+ " Pressure Change : 0.0000 : pascal : True : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " flow_mol mole / second 1.0000 0.51773 0.48227 \n",
+ " mole_frac_comp benzene dimensionless 0.50000 0.60690 0.38524 \n",
+ " mole_frac_comp toluene dimensionless 0.50000 0.39310 0.61476 \n",
+ " temperature kelvin 368.00 368.85 368.85 \n",
+ " pressure pascal 1.0132e+05 1.0132e+05 1.0132e+05 \n",
+ "====================================================================================\n"
+ ]
+ }
+ ],
+ "execution_count": 45
},
{
- "cell_type": "code",
- "execution_count": null,
"metadata": {
+ "ExecuteTime": {
+ "end_time": "2025-06-06T17:05:05.768445Z",
+ "start_time": "2025-06-06T17:05:05.378930Z"
+ },
"tags": [
"solution"
]
},
- "outputs": [],
+ "cell_type": "code",
"source": [
"# re-initialize the model - this may or may not be required depending on current state but safe to initialize\n",
"m.fs.flash.heat_duty.fix(0)\n",
@@ -868,7 +1932,102 @@
"\n",
"# Check stream condition\n",
"m.fs.flash.report()"
- ]
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "WARNING: model contains export suffix 'scaling_factor' that contains 6\n",
+ "component keys that are not exported as part of the NL file. Skipping.\n",
+ "Ipopt 3.13.2: \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...: 137\n",
+ "Number of nonzeros in inequality constraint Jacobian.: 0\n",
+ "Number of nonzeros in Lagrangian Hessian.............: 72\n",
+ "\n",
+ "Total number of variables............................: 42\n",
+ " variables with only lower bounds: 3\n",
+ " variables with lower and upper bounds: 10\n",
+ " variables with only upper bounds: 0\n",
+ "Total number of equality constraints.................: 42\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 3.40e-02 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n",
+ " 1 0.0000000e+00 1.64e+02 7.01e-02 -1.0 5.15e+03 - 9.87e-01 1.00e+00h 1\n",
+ " 2 0.0000000e+00 9.59e-02 2.03e-03 -1.0 7.07e+01 - 9.90e-01 1.00e+00h 1\n",
+ " 3 0.0000000e+00 6.96e-08 2.50e-06 -1.0 4.13e-01 - 9.98e-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....: 8.9151738215745240e-11 6.9550878833979368e-08\n",
+ "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n",
+ "Overall NLP error.......: 8.9151738215745240e-11 6.9550878833979368e-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.001\n",
+ "Total CPU secs in NLP function evaluations = 0.000\n",
+ "\n",
+ "EXIT: Optimal Solution Found.\n",
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.flash Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : 5083.6 : watt : False : (None, None)\n",
+ " Pressure Change : 0.0000 : pascal : True : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " flow_mol mole / second 1.0000 0.54833 0.45167 \n",
+ " mole_frac_comp benzene dimensionless 0.50000 0.60000 0.37860 \n",
+ " mole_frac_comp toluene dimensionless 0.50000 0.40000 0.62140 \n",
+ " temperature kelvin 368.00 369.07 369.07 \n",
+ " pressure pascal 1.0132e+05 1.0132e+05 1.0132e+05 \n",
+ "====================================================================================\n"
+ ]
+ }
+ ],
+ "execution_count": 46
}
],
"metadata": {
@@ -893,4 +2052,4 @@
},
"nbformat": 4,
"nbformat_minor": 3
-}
+}
\ No newline at end of file
diff --git a/idaes_examples/notebooks/docs/tut/core/hda_flowsheet.ipynb b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet.ipynb
index 4f11e1c6..75c1582a 100644
--- a/idaes_examples/notebooks/docs/tut/core/hda_flowsheet.ipynb
+++ b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet.ipynb
@@ -2,18 +2,20 @@
"cells": [
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"header",
"hide-cell"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:27.028013200Z",
+ "start_time": "2026-01-13T00:12:27.015275700Z"
+ }
},
- "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",
+ "# 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",
@@ -23,7 +25,9 @@
"# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n",
"# for full copyright and license information.\n",
"###############################################################################"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 1
},
{
"cell_type": "markdown",
@@ -32,20 +36,34 @@
"\n",
"# HDA Flowsheet Simulation and Optimization\n",
"\n",
- "Author: Jaffer Ghouse \n",
- "Maintainer: Brandon Paul \n",
- "Updated: 2023-06-01 \n",
+ "Author: Jaffer Ghouse
\n",
+ "Maintainer: Tanner Polley
\n",
+ "Updated: 2026-1-12\n",
"\n",
"## Learning outcomes\n",
"\n",
"\n",
"- Construct a steady-state flowsheet using the IDAES unit model library\n",
- "- Connecting unit models in a flowsheet using Arcs\n",
+ "- Connecting unit models in a flowsheet using Arcs\n",
"- Using the SequentialDecomposition tool to initialize a flowsheet with recycle\n",
"- Formulate and solve an optimization problem\n",
" - Defining an objective function\n",
" - Setting variable bounds\n",
- " - Adding additional constraints \n",
+ " - Adding additional constraints\n",
+ "\n",
+ "\n",
+ "The general workflow of setting up an IDAES flowsheet is the following:\n",
+ "\n",
+ " 1 Importing Modules
\n",
+ " 2 Building a Model
\n",
+ " 3 Scaling the Model
\n",
+ " 4 Specifying the Model
\n",
+ " 5 Initializing the Model
\n",
+ " 6 Solving the Model
\n",
+ " 7 Analyzing and Visualizing the Results
\n",
+ " 8 Optimizing the Model
\n",
+ "\n",
+ "We will complete each of these steps as well as demonstrate analyses on this model through some examples and exercises.\n",
"\n",
"\n",
"## Problem Statement\n",
@@ -81,10 +99,11 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Importing required pyomo and idaes components\n",
+ "## 1 Importing Modules\n",
+ "### 1.1 Importing required Pyomo and IDAES components\n",
"\n",
"\n",
- "To construct a flowsheet, we will need several components from the pyomo and idaes package. Let us first import the following components from Pyomo:\n",
+ "To construct a flowsheet, we will need several components from the Pyomo and IDAES package. 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",
@@ -95,14 +114,17 @@
"- Arc (to connect two unit models)\n",
"- SequentialDecomposition (to initialize the flowsheet in a sequential mode)\n",
"\n",
- "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\n"
+ "For further details on these components, please refer to the Pyomo documentation: https://Pyomo.readthedocs.io/en/stable/\n"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:27.614407500Z",
+ "start_time": "2026-01-13T00:12:27.029098600Z"
+ }
+ },
"source": [
"from pyomo.environ import (\n",
" Constraint,\n",
@@ -110,40 +132,49 @@
" ConcreteModel,\n",
" Expression,\n",
" Objective,\n",
- " SolverFactory,\n",
" TransformationFactory,\n",
" value,\n",
")\n",
- "from pyomo.network import Arc, SequentialDecomposition"
- ]
+ "from pyomo.network import Arc"
+ ],
+ "outputs": [],
+ "execution_count": 2
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "From idaes, we will be needing the FlowsheetBlock and the following unit models:\n",
+ "From IDAES, we will be needing the FlowsheetBlock and the following unit models:\n",
+ "- Feed\n",
"- Mixer\n",
"- Heater\n",
"- StoichiometricReactor\n",
"-
**Flash**\n",
"- Separator (splitter) \n",
- "- PressureChanger"
+ "- PressureChanger\n",
+ "- Product"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.427113300Z",
+ "start_time": "2026-01-13T00:12:27.699286500Z"
+ }
+ },
+ "source": "from idaes.core import FlowsheetBlock",
"outputs": [],
- "source": [
- "from idaes.core import FlowsheetBlock"
- ]
+ "execution_count": 3
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.495404700Z",
+ "start_time": "2026-01-13T00:12:29.432738900Z"
+ }
+ },
"source": [
"from idaes.models.unit_models import (\n",
" PressureChanger,\n",
@@ -151,8 +182,12 @@
" Separator as Splitter,\n",
" Heater,\n",
" StoichiometricReactor,\n",
+ " Feed,\n",
+ " Product,\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 4
},
{
"cell_type": "markdown",
@@ -166,30 +201,38 @@
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"exercise"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.504917Z",
+ "start_time": "2026-01-13T00:12:29.497935600Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: import flash model from idaes.models.unit_models"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 5
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"solution"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.528819Z",
+ "start_time": "2026-01-13T00:12:29.505935Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: import flash model from idaes.models.unit_models\n",
"from idaes.models.unit_models import Flash"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 6
},
{
"cell_type": "markdown",
@@ -200,24 +243,27 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.539485800Z",
+ "start_time": "2026-01-13T00:12:29.531348400Z"
+ }
+ },
"source": [
"from idaes.models.unit_models.pressure_changer import ThermodynamicAssumption\n",
"from idaes.core.util.model_statistics import degrees_of_freedom\n",
"\n",
"# Import idaes logger to set output levels\n",
- "import idaes.logger as idaeslog\n",
- "from idaes.core.solvers import get_solver\n",
- "from idaes.core.util.exceptions import InitializationError"
- ]
+ "from idaes.core.solvers import get_solver"
+ ],
+ "outputs": [],
+ "execution_count": 7
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Importing required thermo and reaction package\n",
+ "### 1.2 Importing required thermo and reaction package\n",
"\n",
"The final set of imports are to import the thermo and reaction package for the HDA process. We have created a custom thermo package that assumes Ideal Gas with support for VLE. \n",
"\n",
@@ -232,33 +278,49 @@
]
},
{
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.627980300Z",
+ "start_time": "2026-01-13T00:12:29.540486900Z"
+ }
+ },
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
"source": [
- "from idaes_examples.mod.hda import hda_ideal_VLE as thermo_props\n",
- "from idaes_examples.mod.hda import hda_reaction as reaction_props"
- ]
+ "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_examples.mod.hda.hda_ideal_VLE_modular import thermo_config\n",
+ "from idaes_examples.mod.hda.hda_reaction_modular import reaction_config"
+ ],
+ "outputs": [],
+ "execution_count": 8
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Constructing the Flowsheet\n",
+ "## 2 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 as we did in module 1. "
+ "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": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.638672500Z",
+ "start_time": "2026-01-13T00:12:29.630018Z"
+ }
+ },
"source": [
"m = ConcreteModel()\n",
"m.fs = FlowsheetBlock(dynamic=False)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 9
},
{
"cell_type": "markdown",
@@ -269,34 +331,45 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.669834600Z",
+ "start_time": "2026-01-13T00:12:29.638672500Z"
+ }
+ },
"source": [
- "m.fs.thermo_params = thermo_props.HDAParameterBlock()\n",
- "m.fs.reaction_params = reaction_props.HDAReactionParameterBlock(\n",
- " property_package=m.fs.thermo_params\n",
+ "m.fs.thermo_params = GenericParameterBlock(**thermo_config)\n",
+ "m.fs.reaction_params = GenericReactionParameterBlock(\n",
+ " property_package=m.fs.thermo_params, **reaction_config\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 10
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Adding Unit Models\n",
+ "### 2.1 Adding Unit Models\n",
"\n",
- "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding the Mixer (assigned a name M101) and a Heater (assigned a name H101). 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 (https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/unit_models/index.html). For example, the Mixer unit model here is given a `list` consisting of names to the three inlets. "
+ "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding the Feed (assigned a name `I101` for Inlet), `Mixer` (assigned a name `M101`) and a `Heater` (assigned a name `H101`). 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 (https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/unit_models/index.html). For example, the `Mixer` unit model here must be specified the number of inlets that it will take in and the `Heater` can have specific settings enabled such as `has_pressure_change` or `has_phase_equilibrium`."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.718708300Z",
+ "start_time": "2026-01-13T00:12:29.672813900Z"
+ }
+ },
"source": [
+ "m.fs.I101 = Feed(property_package=m.fs.thermo_params)\n",
+ "m.fs.I102 = Feed(property_package=m.fs.thermo_params)\n",
+ "\n",
"m.fs.M101 = Mixer(\n",
" property_package=m.fs.thermo_params,\n",
- " inlet_list=[\"toluene_feed\", \"hydrogen_feed\", \"vapor_recycle\"],\n",
+ " num_inlets=3,\n",
")\n",
"\n",
"m.fs.H101 = Heater(\n",
@@ -304,11 +377,17 @@
" has_pressure_change=False,\n",
" has_phase_equilibrium=True,\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 11
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
"source": [
"
\n",
"
Inline Exercise:\n",
@@ -325,26 +404,32 @@
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"exercise"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.737260500Z",
+ "start_time": "2026-01-13T00:12:29.719754300Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Add reactor with the specifications above"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 12
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"solution"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.761429600Z",
+ "start_time": "2026-01-13T00:12:29.739837200Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Add reactor with the specifications above\n",
"m.fs.R101 = StoichiometricReactor(\n",
@@ -354,7 +439,9 @@
" has_heat_transfer=True,\n",
" has_pressure_change=False,\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 13
},
{
"cell_type": "markdown",
@@ -370,29 +457,37 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.785139300Z",
+ "start_time": "2026-01-13T00:12:29.762955300Z"
+ }
+ },
"source": [
"m.fs.F101 = Flash(\n",
" property_package=m.fs.thermo_params,\n",
" has_heat_transfer=True,\n",
" has_pressure_change=True,\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 14
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Let us now add the Splitter(S101), PressureChanger(C101) and the second Flash(F102). "
+ "Let us now add the Splitter(S101) with specific names for its output (purge and recycle), PressureChanger(C101) and the second Flash(F102)."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.827217600Z",
+ "start_time": "2026-01-13T00:12:29.787651900Z"
+ }
+ },
"source": [
"m.fs.S101 = Splitter(\n",
" property_package=m.fs.thermo_params,\n",
@@ -412,66 +507,113 @@
" has_heat_transfer=True,\n",
" has_pressure_change=True,\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 15
},
{
"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(M101) to the inlet of the heater(H101). "
+ "Last, we will add the three Product blocks (P101, P102, P103). We use `Feed` blocks and `Product` blocks for convenience with reporting stream summaries and consistency"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.844845200Z",
+ "start_time": "2026-01-13T00:12:29.829763300Z"
+ }
+ },
+ "source": [
+ "m.fs.P101 = Product(property_package=m.fs.thermo_params)\n",
+ "m.fs.P102 = Product(property_package=m.fs.thermo_params)\n",
+ "m.fs.P103 = Product(property_package=m.fs.thermo_params)"
+ ],
"outputs": [],
+ "execution_count": 16
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
"source": [
- "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)"
+ "### 2.2 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 inlets (I101, I102) to the inlet of the mixer (M101) and outlet of the mixer to the inlet of the heater(H101)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "\n",
- " \n",
- "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.855598100Z",
+ "start_time": "2026-01-13T00:12:29.847349400Z"
+ }
+ },
+ "source": [
+ "m.fs.s01 = Arc(source=m.fs.I101.outlet, destination=m.fs.M101.inlet_1)\n",
+ "m.fs.s02 = Arc(source=m.fs.I102.outlet, destination=m.fs.M101.inlet_2)\n",
+ "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)"
+ ],
+ "outputs": [],
+ "execution_count": 17
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
+ "source": [
"
\n",
"Inline Exercise:\n",
- "Now, connect the H101 outlet to the R101 inlet using the cell above as a guide. \n",
- "
\n",
- "\n"
+ "Now, connect the H101 outlet to the R101 inlet using the cell above as a guide.\n",
+ "
"
]
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"exercise"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.865134200Z",
+ "start_time": "2026-01-13T00:12:29.857100Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Connect the H101 outlet to R101 inlet"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 18
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"solution"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.873064300Z",
+ "start_time": "2026-01-13T00:12:29.865134200Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Connect the H101 outlet to R101 inlet\n",
"m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 19
},
{
"cell_type": "markdown",
@@ -482,17 +624,45 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.882527100Z",
+ "start_time": "2026-01-13T00:12:29.874065200Z"
+ }
+ },
"source": [
"m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.F101.inlet)\n",
"m.fs.s06 = Arc(source=m.fs.F101.vap_outlet, destination=m.fs.S101.inlet)\n",
+ "m.fs.s07 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet)\n",
"m.fs.s08 = Arc(source=m.fs.S101.recycle, destination=m.fs.C101.inlet)\n",
- "m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.vapor_recycle)\n",
- "m.fs.s10 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet)"
+ "m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.inlet_3)"
+ ],
+ "outputs": [],
+ "execution_count": 20
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Last we will connect the outlet streams to the inlets of the Product blocks (P101, P102, P103)"
]
},
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.891441800Z",
+ "start_time": "2026-01-13T00:12:29.882527100Z"
+ }
+ },
+ "source": [
+ "m.fs.s10 = Arc(source=m.fs.F102.vap_outlet, destination=m.fs.P101.inlet)\n",
+ "m.fs.s11 = Arc(source=m.fs.F102.liq_outlet, destination=m.fs.P102.inlet)\n",
+ "m.fs.s12 = Arc(source=m.fs.S101.purge, destination=m.fs.P103.inlet)"
+ ],
+ "outputs": [],
+ "execution_count": 21
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -502,20 +672,25 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.916831200Z",
+ "start_time": "2026-01-13T00:12:29.891441800Z"
+ }
+ },
"source": [
"TransformationFactory(\"network.expand_arcs\").apply_to(m)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 22
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Adding expressions to compute purity and operating costs\n",
+ "### 2.3 Adding expressions to compute purity and 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: https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html\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: https://pyomo.readthedocs.io/en/stable/explanation/modeling/network.html.\n",
"\n",
"For this flowsheet, we are interested in computing the purity of the product Benzene stream (i.e. the mole fraction) and the operating cost which is a sum of the cooling and heating cost. "
]
@@ -529,18 +704,27 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.926950800Z",
+ "start_time": "2026-01-13T00:12:29.918895100Z"
+ }
+ },
"source": [
"m.fs.purity = Expression(\n",
- " expr=m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
+ " expr=m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"benzene\"\n",
+ " ]\n",
" / (\n",
- " m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- " + m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
+ " m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\"Vap\", \"benzene\"]\n",
+ " + m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"toluene\"\n",
+ " ]\n",
" )\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 23
},
{
"cell_type": "markdown",
@@ -551,14 +735,19 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.937452200Z",
+ "start_time": "2026-01-13T00:12:29.928143Z"
+ }
+ },
"source": [
"m.fs.cooling_cost = Expression(\n",
" expr=0.212e-7 * (-m.fs.F101.heat_duty[0]) + 0.212e-7 * (-m.fs.R101.heat_duty[0])\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 24
},
{
"cell_type": "markdown",
@@ -575,14 +764,19 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.945656300Z",
+ "start_time": "2026-01-13T00:12:29.938460700Z"
+ }
+ },
"source": [
"m.fs.heating_cost = Expression(\n",
" expr=2.2e-7 * m.fs.H101.heat_duty[0] + 1.9e-7 * m.fs.F102.heat_duty[0]\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 25
},
{
"cell_type": "markdown",
@@ -593,123 +787,118 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.954918200Z",
+ "start_time": "2026-01-13T00:12:29.947160400Z"
+ }
+ },
"source": [
"m.fs.operating_cost = Expression(\n",
" expr=(3600 * 24 * 365 * (m.fs.heating_cost + m.fs.cooling_cost))\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 26
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Fixing feed conditions\n",
+ "## 4 Specifying the Model\n",
+ "### 4.1 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. "
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.987120200Z",
+ "start_time": "2026-01-13T00:12:29.954918200Z"
+ }
+ },
"source": [
"print(degrees_of_freedom(m))"
- ]
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "29\n"
+ ]
+ }
+ ],
+ "execution_count": 27
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"testing"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.023856500Z",
+ "start_time": "2026-01-13T00:12:29.995698600Z"
+ }
},
- "outputs": [],
"source": [
"# Check the degrees of freedom\n",
"assert degrees_of_freedom(m) == 29"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 28
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "We will now be fixing the toluene feed stream to the conditions shown in the flowsheet above. Please note that though this is a pure toluene feed, the remaining components are still assigned a very small non-zero value to help with convergence and initializing. "
+ "We will now be fixing the toluene feed (`I101`) stream to the conditions shown in the flowsheet above. Please note\n",
+ "that though this is a pure toluene feed, the remaining components are still assigned a very small non-zero value to\n",
+ "help with convergence and initializing. We will be importing a function that will specify the inlet conditions for\n",
+ "this example."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"toluene\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"hydrogen\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"methane\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"toluene\"].fix(0.30)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"hydrogen\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"methane\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.temperature.fix(303.2)\n",
- "m.fs.M101.toluene_feed.pressure.fix(350000)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.037749600Z",
+ "start_time": "2026-01-13T00:12:30.025896900Z"
+ }
+ },
"source": [
+ "from idaes_examples.mod.hda.hda_flowsheet_extras import fix_inlet_states\n",
"\n",
- "Similarly, let us fix the hydrogen feed to the following conditions in the next cell:\n",
- "
\n",
- " - FH2 = 0.30 mol/s
\n",
- " - FCH4 = 0.02 mol/s
\n",
- " - Remaining components = 1e-5 mol/s
\n",
- " - T = 303.2 K
\n",
- " - P = 350000 Pa
\n",
- "
\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
+ "tear_guesses = fix_inlet_states(m)"
+ ],
"outputs": [],
- "source": [
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"toluene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"hydrogen\"].fix(0.30)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"methane\"].fix(0.02)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"toluene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"hydrogen\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"methane\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.temperature.fix(303.2)\n",
- "m.fs.M101.hydrogen_feed.pressure.fix(350000)"
- ]
+ "execution_count": 29
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Fixing unit model specifications\n",
+ "### 4.2 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 set set the H101 outlet temperature to 600 K. "
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.047997400Z",
+ "start_time": "2026-01-13T00:12:30.039779600Z"
+ }
+ },
"source": [
"m.fs.H101.outlet.temperature.fix(600)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 30
},
{
"cell_type": "markdown",
@@ -720,23 +909,31 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.057083400Z",
+ "start_time": "2026-01-13T00:12:30.047997400Z"
+ }
+ },
"source": [
"m.fs.R101.conversion = Var(initialize=0.75, bounds=(0, 1))\n",
"\n",
"m.fs.R101.conv_constraint = Constraint(\n",
- " expr=m.fs.R101.conversion * m.fs.R101.inlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
+ " expr=m.fs.R101.conversion\n",
+ " * (m.fs.R101.control_volume.properties_in[0].flow_mol_phase_comp[\"Vap\", \"toluene\"])\n",
" == (\n",
- " m.fs.R101.inlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
- " - m.fs.R101.outlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
+ " m.fs.R101.control_volume.properties_in[0].flow_mol_phase_comp[\"Vap\", \"toluene\"]\n",
+ " - m.fs.R101.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"toluene\"\n",
+ " ]\n",
" )\n",
")\n",
"\n",
"m.fs.R101.conversion.fix(0.75)\n",
"m.fs.R101.heat_duty.fix(0)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 31
},
{
"cell_type": "markdown",
@@ -747,17 +944,26 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.064658700Z",
+ "start_time": "2026-01-13T00:12:30.057083400Z"
+ }
+ },
"source": [
"m.fs.F101.vap_outlet.temperature.fix(325.0)\n",
"m.fs.F101.deltaP.fix(0)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 32
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
"source": [
"
\n",
"
Inline Exercise:\n",
@@ -773,30 +979,38 @@
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"exercise"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.072457700Z",
+ "start_time": "2026-01-13T00:12:30.064658700Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Set conditions for Flash F102"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 33
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"solution"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.079966500Z",
+ "start_time": "2026-01-13T00:12:30.072457700Z"
+ }
},
- "outputs": [],
"source": [
"m.fs.F102.vap_outlet.temperature.fix(375)\n",
"m.fs.F102.deltaP.fix(-200000)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 34
},
{
"cell_type": "markdown",
@@ -807,17 +1021,26 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.088622800Z",
+ "start_time": "2026-01-13T00:12:30.079966500Z"
+ }
+ },
"source": [
"m.fs.S101.split_fraction[0, \"purge\"].fix(0.2)\n",
"m.fs.C101.outlet.pressure.fix(350000)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 35
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
"source": [
"
\n",
"
Inline Exercise:\n",
@@ -829,343 +1052,976 @@
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"exercise"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.096269500Z",
+ "start_time": "2026-01-13T00:12:30.088622800Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: print the degrees of freedom"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 36
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"solution"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.125313600Z",
+ "start_time": "2026-01-13T00:12:30.097272200Z"
+ }
},
- "outputs": [],
"source": [
"print(degrees_of_freedom(m))"
- ]
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0\n"
+ ]
+ }
+ ],
+ "execution_count": 37
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"testing"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.164032900Z",
+ "start_time": "2026-01-13T00:12:30.134372Z"
+ }
},
- "outputs": [],
"source": [
"# Check the degrees of freedom\n",
"assert degrees_of_freedom(m) == 0"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 38
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Initialization\n",
+ "## 5 Initializing the Model\n",
"\n",
"\n",
- "This section will demonstrate how to use the built-in sequential decomposition tool to initialize our flowsheet.\n",
"\n",
- " \n"
+ "When a flowsheet contains a recycle loop, the outlet of a downstream unit becomes the inlet of an upstream unit, creating a cyclic dependency that prevents straightforward calculation of all stream conditions. The tear‐stream method is necessary because it “breaks” this loop: you select one recycle stream as the tear, assign it an initial guess, and then solve the rest of the flowsheet as if it were acyclic. Once the downstream units compute their outputs, you compare the calculated value of the torn stream to your initial guess and iteratively adjust until they coincide. Without tearing, the solver cannot establish a proper topological sequence or drive the recycle to convergence, making initialization—and ultimately steady‐state convergence—impossible.\n",
+ "\n",
+ "It is important to determine the tear stream for a flowsheet which will be demonstrated below.\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "Currently, there are two methods of initializing a full flowsheet: using the sequential decomposition tool, or\n",
+ "manually propagating through the flowsheet. The tear stream in this example will be the stream from the mixer to the heater since that is where the\n",
+ "recycle stream first enters back into the main process.\n",
+ "\n"
]
},
{
- "cell_type": "markdown",
"metadata": {},
- "source": [
- "Let us first create an object for the SequentialDecomposition and specify our options for this. "
- ]
+ "cell_type": "markdown",
+ "source": "First, we will highlight some helpful functions that are used in the initialization process."
},
{
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.174558500Z",
+ "start_time": "2026-01-13T00:12:30.166118800Z"
+ }
+ },
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
"source": [
- "seq = SequentialDecomposition()\n",
- "seq.options.select_tear_method = \"heuristic\"\n",
- "seq.options.tear_method = \"Wegstein\"\n",
- "seq.options.iterLim = 3\n",
+ "def initialize_unit(unit):\n",
+ " from idaes.core.util.exceptions import InitializationError\n",
+ " import idaes.logger as idaeslog\n",
"\n",
- "# Using the SD tool\n",
- "G = seq.create_graph(m)\n",
- "heuristic_tear_set = seq.tear_set_arcs(G, method=\"heuristic\")\n",
- "order = seq.calculation_order(G)"
- ]
+ " optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 1000,\n",
+ " \"tol\": 1e-8,\n",
+ " }\n",
+ "\n",
+ " try:\n",
+ " initializer = unit.default_initializer(solver_options=optarg)\n",
+ " initializer.initialize(unit, output_level=idaeslog.INFO_LOW)\n",
+ " except InitializationError:\n",
+ " solver = get_solver(solver_options=optarg)\n",
+ " solver.solve(unit)"
+ ],
+ "outputs": [],
+ "execution_count": 39
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Which is the tear stream? Display tear set and order"
+ "This first function will take any unit model and can either initialize the model with its respective default\n",
+ "initializer, or use a generic solver and the solve the current state of the unit model. Often times a direct\n",
+ "initialization method will fail while a solving method will converge so having the option for both is helpful.\n",
+ "\n",
+ "\n",
+ "### 5.1 Sequential Decomposition\n",
+ "\n",
+ "This section will demonstrate how to use the built-in sequential decomposition tool to initialize our flowsheet. Sequential Decomposition is a tool from Pyomo where the documentation can be found here https://Pyomo.readthedocs.io/en/stable/explanation/modeling/network.html#sequential-decomposition"
]
},
{
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.183526400Z",
+ "start_time": "2026-01-13T00:12:30.175559400Z"
+ }
+ },
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
"source": [
- "for o in heuristic_tear_set:\n",
- " print(o.name)"
- ]
+ "def automatic_propagation(m, tear_guesses):\n",
+ "\n",
+ " from pyomo.network import SequentialDecomposition\n",
+ "\n",
+ " seq = SequentialDecomposition()\n",
+ " seq.options.select_tear_method = \"heuristic\"\n",
+ " seq.options.tear_method = \"Wegstein\"\n",
+ " seq.options.iterLim = 5\n",
+ "\n",
+ " # Using the SD tool\n",
+ " G = seq.create_graph(m)\n",
+ " heuristic_tear_set = seq.tear_set_arcs(G, method=\"heuristic\")\n",
+ " order = seq.calculation_order(G)\n",
+ "\n",
+ " # Pass the tear_guess to the SD tool\n",
+ " seq.set_guesses_for(heuristic_tear_set[0].destination, tear_guesses)\n",
+ "\n",
+ " print(f\"Tear Stream starts at: {heuristic_tear_set[0].destination.name}\")\n",
+ "\n",
+ " for o in order:\n",
+ " print(o[0].name)\n",
+ "\n",
+ " seq.run(m, initialize_unit)"
+ ],
+ "outputs": [],
+ "execution_count": 40
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "What sequence did the SD tool determine to solve this flowsheet with the least number of tears? "
+ "We are now ready to initialize our flowsheet in a sequential mode. Note that we specifically set the iteration limit\n",
+ "to be 5 as we are trying to use this tool only to get a good set of initial values such that IPOPT can then take over\n",
+ " and solve this flowsheet for us. Uncomment this function call to run the automatic propagation method"
]
},
{
- "cell_type": "code",
- "execution_count": null,
"metadata": {
- "scrolled": true
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.191140600Z",
+ "start_time": "2026-01-13T00:12:30.184028800Z"
+ }
},
+ "cell_type": "code",
+ "source": "# automatic_propagation(m, tear_guesses)",
"outputs": [],
- "source": [
- "for o in order:\n",
- " print(o[0].name)"
- ]
+ "execution_count": 41
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- " \n",
- "\n",
- " \n",
- "\n",
+ "### 5.2 Manual Propagation Method\n",
"\n",
- "The SequentialDecomposition tool has determined that the tear stream is the mixer outlet. We will need to provide a reasonable guess for this."
+ "This method uses a more direct approach to initialize the flowsheet, using the updated initializer method and propagating manually through the flowsheet and solving for the tear stream directly.\n",
+ "Lets define the function that will help us manually propagate and step through the flowsheet"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.202920300Z",
+ "start_time": "2026-01-13T00:12:30.191140600Z"
+ }
+ },
"source": [
- "tear_guesses = {\n",
- " \"flow_mol_phase_comp\": {\n",
- " (0, \"Vap\", \"benzene\"): 1e-5,\n",
- " (0, \"Vap\", \"toluene\"): 1e-5,\n",
- " (0, \"Vap\", \"hydrogen\"): 0.30,\n",
- " (0, \"Vap\", \"methane\"): 0.02,\n",
- " (0, \"Liq\", \"benzene\"): 1e-5,\n",
- " (0, \"Liq\", \"toluene\"): 0.30,\n",
- " (0, \"Liq\", \"hydrogen\"): 1e-5,\n",
- " (0, \"Liq\", \"methane\"): 1e-5,\n",
- " },\n",
- " \"temperature\": {0: 303},\n",
- " \"pressure\": {0: 350000},\n",
- "}\n",
- "\n",
- "# Pass the tear_guess to the SD tool\n",
- "seq.set_guesses_for(m.fs.H101.inlet, tear_guesses)"
- ]
+ "def manual_propagation(m, tear_guesses):\n",
+ " from idaes.core.util.initialization import propagate_state\n",
+ "\n",
+ " print(f\"The DOF is {degrees_of_freedom(m)} initially\")\n",
+ " m.fs.s03_expanded.deactivate()\n",
+ " print(f\"The DOF is {degrees_of_freedom(m)} after deactivating the tear stream\")\n",
+ "\n",
+ " for k, v in tear_guesses.items():\n",
+ " for k1, v1 in v.items():\n",
+ " getattr(m.fs.s03.destination, k)[k1].fix(v1)\n",
+ "\n",
+ " print(f\"The DOF is {degrees_of_freedom(m)} after setting the tear stream\")\n",
+ "\n",
+ " optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 300,\n",
+ " # \"tol\": 1e-10,\n",
+ " }\n",
+ "\n",
+ " solver = get_solver(solver_options=optarg)\n",
+ "\n",
+ " initialize_unit(m.fs.H101) # Initialize Heater\n",
+ " propagate_state(m.fs.s04) # Establish connection between Heater and Reactor\n",
+ " initialize_unit(m.fs.R101) # Initialize Reactor\n",
+ " propagate_state(\n",
+ " m.fs.s05\n",
+ " ) # Establish connection between Reactor and First Flash Unit\n",
+ " initialize_unit(m.fs.F101) # Initialize First Flash Unit\n",
+ " propagate_state(\n",
+ " m.fs.s06\n",
+ " ) # Establish connection between First Flash Unit and Splitter\n",
+ " propagate_state(\n",
+ " m.fs.s07\n",
+ " ) # Establish connection between First Flash Unit and Second Flash Unit\n",
+ " initialize_unit(m.fs.S101) # Initialize Splitter\n",
+ " propagate_state(m.fs.s08) # Establish connection between Splitter and Compressor\n",
+ " initialize_unit(m.fs.C101) # Initialize Compressor\n",
+ " propagate_state(m.fs.s09) # Establish connection between Compressor and Mixer\n",
+ " initialize_unit(m.fs.I101) # Initialize Toluene Inlet\n",
+ " propagate_state(m.fs.s01) # Establish connection between Toluene Inlet and Mixer\n",
+ " initialize_unit(m.fs.I102) # Initialize Hydrogen Inlet\n",
+ " propagate_state(m.fs.s02) # Establish connection between Hydrogen Inlet and Mixer\n",
+ " initialize_unit(m.fs.M101) # Initialize Mixer\n",
+ " propagate_state(m.fs.s03) # Establish connection between Mixer and Heater\n",
+ " solver.solve(m.fs.F102)\n",
+ " propagate_state(\n",
+ " m.fs.s10\n",
+ " ) # Establish connection between Second Flash Unit and Benzene Product\n",
+ " propagate_state(\n",
+ " m.fs.s11\n",
+ " ) # Establish connection between Second Flash Unit and Toluene Product\n",
+ " propagate_state(m.fs.s12) # Establish connection between Splitter and Purge Product\n",
+ "\n",
+ " optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 300,\n",
+ " \"tol\": 1e-8,\n",
+ " }\n",
+ " solver = get_solver(\"ipopt_v2\", options=optarg)\n",
+ " solver.solve(m, tee=False)\n",
+ "\n",
+ " for k, v in tear_guesses.items():\n",
+ " for k1, v1 in v.items():\n",
+ " getattr(m.fs.H101.inlet, k)[k1].unfix()\n",
+ "\n",
+ " m.fs.s03_expanded.activate()\n",
+ " print(\n",
+ " f\"The DOF is {degrees_of_freedom(m)} after unfixing the values and reactivating the tear stream\"\n",
+ " )"
+ ],
+ "outputs": [],
+ "execution_count": 42
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Next, we need to tell the tool how to initialize a particular unit. We will be writing a python function which takes in a \"unit\" and calls the initialize method on that unit. "
+ "It will first show that the degrees of freedom is correctly at 0 before any streams are deactivated. Once the tear\n",
+ "stream is deactivated though, the degrees of freedom will be 10. That means 10 variables will have to be defined with\n",
+ " the tear guesses `tear_guesses`. Then each unit model can be initialized with our same helper function and then can\n",
+ " propagate the corresponding connection to the following unit models. At the end, the whole flowsheet is solved,\n",
+ " giving a much better chance for the recycle stream to be used correctly the flowsheet to converge."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.448895Z",
+ "start_time": "2026-01-13T00:12:30.204922600Z"
+ }
+ },
+ "source": "manual_propagation(m, tear_guesses)",
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The DOF is 0 initially\n",
+ "The DOF is 10 after deactivating the tear stream\n",
+ "The DOF is 0 after setting the tear stream\n",
+ "The DOF is 0 after unfixing the values and reactivating the tear stream\n"
+ ]
+ }
+ ],
+ "execution_count": 43
+ },
+ {
+ "cell_type": "markdown",
"metadata": {},
- "outputs": [],
"source": [
- "def function(unit):\n",
- " try:\n",
- " initializer = unit.default_initializer()\n",
- " initializer.initialize(unit, output_level=idaeslog.INFO)\n",
- " except InitializationError:\n",
- " solver = get_solver()\n",
- " solver.solve(unit)"
+ "## 6 Solving the Model"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "tags": []
+ },
"source": [
- "We are now ready to initialize our flowsheet in a sequential mode. Note that we specifically set the iteration limit to be 5 as we are trying to use this tool only to get a good set of initial values such that IPOPT can then take over and solve this flowsheet for us. "
+ "We have now initialized the flowsheet. Lets set up some solving options before simulating the flowsheet. We want to specify the scaling method, number of iterations, and tolerance. More specific or advanced options can be found at the documentation for IPOPT https://coin-or.github.io/Ipopt/OPTIONS.html"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.467353Z",
+ "start_time": "2026-01-13T00:12:32.458309200Z"
+ }
+ },
"source": [
- "seq.run(m, function)"
- ]
+ "optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 1000,\n",
+ " \"tol\": 1e-8,\n",
+ "}"
+ ],
+ "outputs": [],
+ "execution_count": 44
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
"source": [
"
\n",
"Inline Exercise:\n",
- "We have now initialized the flowsheet. Let us run the flowsheet in a simulation mode to look at the results. To do this, complete the last line of code where we pass the model to the solver. You will need to type the following:\n",
- " \n",
+ "Let us run the flowsheet in a simulation mode to look at the results. To do this, complete the last line of code where we pass the model to the solver. You will need to type the following:\n",
+ "\n",
+ "solver = get_solver(solver_options=optarg)
\n",
"results = solver.solve(m, tee=True)\n",
"\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
\n",
- "\n"
+ "Use Shift+Enter to run the cell once you have typed in your code.\n",
+ "
\n"
]
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"exercise"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.476284300Z",
+ "start_time": "2026-01-13T00:12:32.467353Z"
+ }
},
- "outputs": [],
"source": [
"# Create the solver object\n",
"\n",
- "\n",
"# Solve the model"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 45
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"solution"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.967264500Z",
+ "start_time": "2026-01-13T00:12:32.476788800Z"
+ }
},
- "outputs": [],
"source": [
"# Create the solver object\n",
- "from idaes.core.solvers import get_solver\n",
- "\n",
- "solver = get_solver()\n",
+ "solver = get_solver(\"ipopt_v2\", options=optarg)\n",
"\n",
"# Solve the model\n",
- "results = solver.solve(m, tee=True)"
- ]
+ "results = solver.solve(m, tee=False)"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Ipopt 3.13.2: linear_solver=\"ma57\"\n",
+ "max_iter=1000\n",
+ "nlp_scaling_method=\"user-scaling\"\n",
+ "tol=1e-08\n",
+ "option_file_name=\"C:\\Users\\Tanner\\AppData\\Local\\Temp\\tmpjzf4e172\\unknown.31496.43076.opt\"\n",
+ "\n",
+ "Using option file \"C:\\Users\\Tanner\\AppData\\Local\\Temp\\tmpjzf4e172\\unknown.31496.43076.opt\".\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 ma57.\n",
+ "\n",
+ "Number of nonzeros in equality constraint Jacobian...: 920\n",
+ "Number of nonzeros in inequality constraint Jacobian.: 0\n",
+ "Number of nonzeros in Lagrangian Hessian.............: 456\n",
+ "\n",
+ "Total number of variables............................: 218\n",
+ " variables with only lower bounds: 56\n",
+ " variables with lower and upper bounds: 155\n",
+ " variables with only upper bounds: 0\n",
+ "Total number of equality constraints.................: 218\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 8.17e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n",
+ "Reallocating memory for MA57: lfact (10193)\n",
+ " 1 0.0000000e+00 7.12e+03 2.01e+02 -1.0 4.35e+04 - 4.68e-02 1.56e-01h 1\n",
+ " 2 0.0000000e+00 7.12e+03 2.74e+02 -1.0 4.20e+05 - 9.82e-04 5.24e-04h 1\n",
+ " 3 0.0000000e+00 7.11e+03 2.06e+03 -1.0 4.26e+05 - 1.43e-05 1.49e-03f 1\n",
+ " 4 0.0000000e+00 7.09e+03 1.83e+03 -1.0 3.65e+04 - 5.78e-03 2.02e-03h 1\n",
+ " 5 0.0000000e+00 7.09e+03 1.82e+03 -1.0 1.61e+05 - 4.69e-04 1.23e-05h 1\n",
+ " 6r 0.0000000e+00 7.09e+03 9.99e+02 3.9 0.00e+00 - 0.00e+00 6.02e-08R 2\n",
+ " 7r 0.0000000e+00 6.68e+03 9.98e+02 3.9 7.11e+06 - 5.22e-04 1.63e-04f 1\n",
+ " 8r 0.0000000e+00 4.74e+04 9.98e+02 3.9 3.89e+06 - 2.84e-04 7.21e-04f 1\n",
+ " 9r 0.0000000e+00 4.77e+04 1.78e+04 3.9 1.73e+06 - 4.34e-02 5.81e-04f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 10r 0.0000000e+00 4.55e+04 4.83e+04 3.2 4.71e+04 - 2.92e-01 4.47e-02f 1\n",
+ " 11r 0.0000000e+00 7.86e+04 3.48e+04 3.2 3.13e+02 - 4.51e-01 3.27e-01f 1\n",
+ " 12r 0.0000000e+00 4.85e+04 2.80e+04 3.2 5.27e+01 - 5.22e-01 2.61e-01f 1\n",
+ " 13r 0.0000000e+00 1.68e+05 1.76e+04 3.2 1.46e+02 - 8.30e-01 4.81e-01f 1\n",
+ " 14r 0.0000000e+00 8.02e+04 3.21e+03 3.2 1.42e+02 - 7.28e-01 1.00e+00f 1\n",
+ " 15r 0.0000000e+00 1.89e+05 6.22e+04 3.2 1.06e+02 - 4.83e-01 1.00e+00f 1\n",
+ " 16r 0.0000000e+00 1.77e+05 5.95e+04 3.2 1.08e+02 0.0 9.89e-02 6.28e-02h 1\n",
+ " 17r 0.0000000e+00 1.03e+05 3.98e+04 3.2 8.09e+01 - 7.18e-01 4.22e-01h 1\n",
+ " 18r 0.0000000e+00 8.88e+04 8.50e+04 3.2 1.59e+02 - 2.14e-01 1.36e-01h 1\n",
+ " 19r 0.0000000e+00 5.30e+04 1.71e+06 3.2 8.69e+01 - 3.02e-02 3.51e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 20r 0.0000000e+00 1.40e+04 2.20e+05 3.2 1.08e+01 2.2 6.72e-01 8.78e-01f 1\n",
+ " 21r 0.0000000e+00 9.61e+03 9.51e+04 2.5 1.06e+01 1.8 9.32e-01 4.27e-01f 1\n",
+ " 22r 0.0000000e+00 1.83e+04 3.37e+04 2.5 1.51e+01 1.3 1.00e+00 6.41e-01f 1\n",
+ " 23r 0.0000000e+00 3.76e+03 1.35e+03 2.5 4.90e+00 0.8 1.00e+00 1.00e+00f 1\n",
+ " 24r 0.0000000e+00 1.38e+05 2.72e+04 2.5 1.06e+02 - 3.87e-01 1.00e+00f 1\n",
+ " 25r 0.0000000e+00 1.06e+05 2.11e+04 2.5 5.11e+01 1.2 2.99e-01 2.35e-01h 1\n",
+ " 26r 0.0000000e+00 9.35e+04 1.83e+04 2.5 3.27e+01 2.6 6.94e-02 1.19e-01h 1\n",
+ " 27r 0.0000000e+00 9.23e+03 3.61e+03 2.5 2.06e+01 2.1 2.35e-01 1.00e+00h 1\n",
+ " 28r 0.0000000e+00 9.21e+03 6.60e+03 2.5 9.05e+02 - 1.37e-01 3.89e-03f 1\n",
+ " 29r 0.0000000e+00 6.69e+04 1.34e+04 2.5 1.58e+02 - 3.39e-01 2.01e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 30r 0.0000000e+00 7.42e+01 3.52e+04 2.5 3.26e-01 1.6 4.89e-01 1.00e+00f 1\n",
+ " 31r 0.0000000e+00 9.36e+03 1.66e+02 2.5 1.29e+01 - 1.00e+00 1.00e+00f 1\n",
+ " 32r 0.0000000e+00 9.30e+01 3.75e+00 2.5 1.85e+00 - 1.00e+00 1.00e+00h 1\n",
+ " 33r 0.0000000e+00 8.97e+03 9.16e+02 0.4 4.59e+01 - 7.14e-01 7.41e-01f 1\n",
+ " 34r 0.0000000e+00 7.19e+03 3.37e+02 0.4 2.34e+03 - 7.23e-01 6.68e-01f 1\n",
+ " 35r 0.0000000e+00 5.64e+03 5.42e+02 0.4 7.33e+02 - 6.81e-01 5.42e-01f 1\n",
+ " 36r 0.0000000e+00 1.10e+03 1.79e+02 0.4 2.38e-01 2.0 6.12e-01 8.67e-01f 1\n",
+ " 37r 0.0000000e+00 3.67e+02 1.23e+02 0.4 9.76e-02 2.5 9.90e-01 8.27e-01f 1\n",
+ " 38r 0.0000000e+00 2.93e+01 3.80e+02 0.4 2.55e-01 2.0 5.41e-01 1.00e+00f 1\n",
+ " 39r 0.0000000e+00 2.58e+01 7.52e+02 0.4 2.70e+00 2.4 4.08e-02 9.27e-02h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 40r 0.0000000e+00 1.03e+01 2.85e+02 0.4 8.74e-02 2.8 7.74e-01 6.78e-01f 1\n",
+ " 41r 0.0000000e+00 4.85e+00 8.16e+01 0.4 2.96e-01 2.4 7.04e-01 7.21e-01f 1\n",
+ " 42r 0.0000000e+00 1.15e+02 7.32e+01 -0.3 1.76e-01 1.9 7.14e-01 7.59e-01f 1\n",
+ " 43r 0.0000000e+00 2.10e+02 3.61e+02 -0.3 4.28e-01 1.4 7.77e-01 1.77e-01f 1\n",
+ " 44r 0.0000000e+00 1.51e+02 6.63e+01 -0.3 4.68e-01 0.9 1.00e+00 8.94e-01f 1\n",
+ " 45r 0.0000000e+00 9.19e+00 1.85e+01 -0.3 1.49e-01 1.3 1.00e+00 1.00e+00f 1\n",
+ " 46r 0.0000000e+00 5.62e+00 5.71e+01 -0.3 1.05e+00 0.9 4.17e-01 3.89e-01h 1\n",
+ " 47r 0.0000000e+00 1.98e+01 3.77e+01 -0.3 7.16e-01 0.4 1.00e+00 1.00e+00f 1\n",
+ " 48r 0.0000000e+00 1.82e+01 6.27e+02 -0.3 9.97e+00 -0.1 4.98e-02 8.93e-02h 1\n",
+ " 49r 0.0000000e+00 1.72e+01 5.84e+02 -0.3 1.90e+00 1.2 4.67e-02 4.96e-02f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 50r 0.0000000e+00 1.16e+01 7.04e+02 -0.3 1.10e+00 0.8 6.53e-01 4.12e-01f 1\n",
+ " 51r 0.0000000e+00 1.08e+01 6.28e+02 -0.3 2.94e-01 2.1 3.66e-01 6.78e-02f 1\n",
+ " 52r 0.0000000e+00 6.69e+00 7.88e+02 -0.3 4.74e-01 1.6 5.14e-01 6.14e-01f 1\n",
+ " 53r 0.0000000e+00 6.34e+00 1.39e+03 -0.3 6.49e-02 2.9 1.00e+00 5.17e-02h 1\n",
+ " 54r 0.0000000e+00 5.30e-01 7.34e+02 -0.3 1.91e-01 2.5 5.47e-01 1.00e+00f 1\n",
+ " 55r 0.0000000e+00 5.30e-01 8.37e+02 -0.3 1.09e-01 2.9 1.00e+00 1.86e-01f 1\n",
+ " 56r 0.0000000e+00 6.24e-01 3.25e+02 -0.3 2.18e-01 2.4 7.22e-01 7.62e-01f 1\n",
+ " 57r 0.0000000e+00 5.84e-01 1.25e+03 -0.3 1.10e-01 2.8 3.44e-01 6.58e-02f 1\n",
+ " 58r 0.0000000e+00 1.84e+00 9.86e+02 -0.3 6.56e-01 2.4 4.76e-01 1.66e-01f 1\n",
+ " 59r 0.0000000e+00 9.98e+00 7.31e+02 -0.3 2.98e-01 1.9 3.81e-01 1.96e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 60r 0.0000000e+00 2.33e+01 6.73e+02 -0.3 2.93e+00 1.4 1.64e-03 2.62e-02f 1\n",
+ " 61r 0.0000000e+00 2.09e+02 6.21e+02 -0.3 3.68e+00 1.8 2.80e-02 4.31e-02f 1\n",
+ " 62r 0.0000000e+00 2.06e+02 4.81e+02 -0.3 4.45e-01 1.4 4.03e-01 1.68e-02h 1\n",
+ " 63r 0.0000000e+00 1.65e+02 3.87e+02 -0.3 1.11e+00 0.9 1.92e-02 1.96e-01f 1\n",
+ " 64r 0.0000000e+00 1.50e+02 3.08e+02 -0.3 1.85e+00 0.4 2.04e-01 2.07e-01f 1\n",
+ " 65r 0.0000000e+00 1.42e+02 2.98e+02 -0.3 1.20e+00 1.7 3.04e-02 5.07e-02f 1\n",
+ " 66r 0.0000000e+00 7.46e+01 5.29e+02 -0.3 5.27e-02 2.2 9.16e-01 4.78e-01f 1\n",
+ " 67r 0.0000000e+00 5.57e+01 4.82e+02 -0.3 1.72e-01 1.7 9.61e-01 2.54e-01f 1\n",
+ " 68r 0.0000000e+00 2.98e+00 1.46e+02 -0.3 2.22e-01 1.2 1.00e+00 9.79e-01f 1\n",
+ " 69r 0.0000000e+00 6.01e+00 2.34e+02 -0.3 4.07e-01 0.7 5.24e-01 4.28e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 70r 0.0000000e+00 1.07e+02 5.44e+02 -0.3 1.11e+00 0.3 7.04e-01 1.00e+00f 1\n",
+ " 71r 0.0000000e+00 2.25e+01 7.39e+02 -0.3 1.39e-01 1.6 3.98e-01 7.90e-01f 1\n",
+ " 72r 0.0000000e+00 2.07e+01 7.10e+02 -0.3 6.94e-01 1.1 2.42e-01 8.16e-02f 1\n",
+ " 73r 0.0000000e+00 4.67e+01 3.79e+02 -0.3 3.86e-01 0.6 6.91e-01 1.00e+00f 1\n",
+ " 74r 0.0000000e+00 7.77e+01 2.65e+02 -0.3 9.27e-01 0.2 4.94e-01 4.38e-01h 1\n",
+ " 75r 0.0000000e+00 7.62e+01 2.61e+02 -0.3 3.81e+00 0.6 2.20e-02 3.06e-02f 2\n",
+ " 76r 0.0000000e+00 5.81e+01 2.16e+02 -0.3 1.57e-01 1.9 3.64e-01 2.38e-01f 1\n",
+ " 77r 0.0000000e+00 1.97e+01 1.33e+02 -0.3 1.61e-01 1.4 1.00e+00 6.92e-01f 1\n",
+ " 78r 0.0000000e+00 4.47e+01 2.11e+02 -0.3 8.61e-01 1.0 7.64e-01 4.57e-01f 1\n",
+ " 79r 0.0000000e+00 2.75e+02 1.68e+02 -0.3 2.89e+00 0.5 1.90e-01 1.97e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 80r 0.0000000e+00 1.57e+01 5.93e+01 -0.3 1.06e-01 1.8 1.00e+00 1.00e+00f 1\n",
+ " 81r 0.0000000e+00 4.52e+00 7.54e+00 -0.3 3.54e-01 1.3 1.00e+00 1.00e+00h 1\n",
+ " 82r 0.0000000e+00 1.51e+01 6.83e+01 -1.0 1.48e-01 1.8 6.09e-01 6.86e-01f 1\n",
+ " 83r 0.0000000e+00 9.47e+00 2.62e+01 -1.0 5.68e-02 2.2 1.00e+00 8.94e-01f 1\n",
+ " 84r 0.0000000e+00 2.30e+01 2.17e+01 -1.0 4.35e-01 1.7 1.62e-01 1.76e-01f 1\n",
+ " 85r 0.0000000e+00 1.67e+01 1.83e+02 -1.0 8.29e-02 2.1 1.00e+00 4.80e-01f 1\n",
+ " 86r 0.0000000e+00 5.83e+01 5.12e+02 -1.0 3.20e-01 1.7 6.90e-01 3.58e-01f 1\n",
+ " 87r 0.0000000e+00 1.07e+02 1.19e+03 -1.0 1.30e+01 1.2 2.99e-02 1.02e-02f 1\n",
+ " 88r 0.0000000e+00 1.05e+02 3.60e+02 -1.0 2.66e+00 0.7 1.00e+00 2.59e-02f 1\n",
+ " 89r 0.0000000e+00 4.13e+02 4.81e+02 -1.0 7.27e+00 0.2 1.00e+00 4.22e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 90r 0.0000000e+00 4.12e+02 4.73e+02 -1.0 7.91e+01 -0.3 2.91e-03 3.88e-03f 1\n",
+ " 91r 0.0000000e+00 3.90e+02 5.09e+02 -1.0 1.05e+00 1.1 1.00e+00 5.42e-02f 1\n",
+ " 92r 0.0000000e+00 6.42e+02 1.96e+01 -1.0 3.03e+00 0.6 1.00e+00 1.00e+00f 1\n",
+ " 93r 0.0000000e+00 1.54e+03 5.81e+02 -1.0 1.25e+01 0.1 9.40e-02 6.11e-01f 1\n",
+ " 94r 0.0000000e+00 1.52e+03 5.61e+02 -1.0 9.43e+00 0.5 1.64e-02 8.32e-03f 1\n",
+ " 95r 0.0000000e+00 1.38e+03 3.41e+02 -1.0 2.32e+01 0.1 3.92e-01 1.27e-01f 1\n",
+ " 96r 0.0000000e+00 1.38e+03 3.72e+02 -1.0 5.95e+00 0.5 3.61e-01 4.34e-03h 1\n",
+ " 97r 0.0000000e+00 1.28e+03 2.01e+02 -1.0 1.09e+01 0.0 4.23e-02 1.05e-01f 1\n",
+ " 98r 0.0000000e+00 9.89e+02 4.20e+02 -1.0 4.94e+00 0.4 3.76e-02 2.66e-01f 1\n",
+ " 99r 0.0000000e+00 8.96e+02 4.65e+02 -1.0 2.96e+00 0.9 2.83e-01 9.51e-02f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 100r 0.0000000e+00 8.38e+02 1.08e+03 -1.0 3.79e+00 0.4 5.07e-01 6.96e-02h 1\n",
+ " 101r 0.0000000e+00 1.19e+03 6.44e+02 -1.0 9.81e+00 -0.1 3.71e-01 3.57e-01f 1\n",
+ " 102r 0.0000000e+00 1.19e+03 5.28e+02 -1.0 1.78e+01 -0.6 7.44e-03 6.07e-02f 1\n",
+ " 103r 0.0000000e+00 1.02e+03 3.34e+02 -1.0 2.10e+01 -1.0 4.17e-02 1.21e-01f 1\n",
+ " 104r 0.0000000e+00 9.98e+02 8.36e+02 -1.0 8.35e+00 -0.6 4.71e-01 1.95e-02h 1\n",
+ " 105r 0.0000000e+00 6.83e+02 6.45e+02 -1.0 1.30e+01 -1.1 5.71e-02 2.84e-01f 1\n",
+ " 106r 0.0000000e+00 8.65e+02 1.14e+03 -1.0 8.25e+00 -0.7 1.61e-01 6.21e-01f 1\n",
+ " 107r 0.0000000e+00 7.16e+02 9.70e+02 -1.0 4.28e+00 -0.2 1.93e-01 1.57e-01h 1\n",
+ " 108r 0.0000000e+00 6.31e+02 8.28e+02 -1.0 6.49e+00 -0.7 6.61e-02 1.81e-01h 1\n",
+ " 109r 0.0000000e+00 4.55e+02 1.19e+03 -1.0 2.95e+00 -0.3 1.88e-01 8.74e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 110r 0.0000000e+00 1.83e+02 4.89e+02 -1.0 1.15e-01 1.9 6.47e-01 5.98e-01h 1\n",
+ " 111r 0.0000000e+00 3.73e+01 1.31e+02 -1.0 8.98e-02 1.5 1.00e+00 7.95e-01h 1\n",
+ " 112r 0.0000000e+00 7.86e+00 8.49e+01 -1.0 7.76e-02 1.9 9.93e-01 7.86e-01h 1\n",
+ " 113r 0.0000000e+00 2.86e+00 1.42e+01 -1.0 1.00e-01 1.4 1.00e+00 1.00e+00h 1\n",
+ " 114r 0.0000000e+00 2.89e+00 2.59e+00 -1.0 3.00e-01 0.9 1.00e+00 1.00e+00f 1\n",
+ " 115r 0.0000000e+00 5.21e+01 6.10e+00 -1.0 8.19e-01 0.5 1.00e+00 1.00e+00f 1\n",
+ " 116r 0.0000000e+00 1.36e+02 4.46e+02 -1.0 2.16e+00 -0.0 2.00e-01 5.43e-01f 1\n",
+ " 117r 0.0000000e+00 2.35e+01 1.35e+02 -1.0 3.81e-02 2.2 6.64e-01 8.26e-01h 1\n",
+ " 118r 0.0000000e+00 1.45e+01 6.04e+02 -1.0 4.14e-01 1.7 1.30e-01 6.39e-01f 1\n",
+ " 119r 0.0000000e+00 7.09e+01 1.51e+03 -1.0 1.49e+00 2.2 9.44e-02 3.42e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 120r 0.0000000e+00 6.91e+01 1.74e+03 -1.0 3.65e-01 2.6 2.02e-01 2.71e-02f 1\n",
+ " 121r 0.0000000e+00 6.89e+01 1.47e+03 -1.0 1.29e+00 2.1 8.23e-04 3.05e-03f 1\n",
+ " 122r 0.0000000e+00 6.81e+01 2.26e+03 -1.0 1.01e+00 1.6 5.80e-02 1.11e-02f 1\n",
+ " 123r 0.0000000e+00 5.53e+01 8.13e+02 -1.0 9.48e-01 1.2 1.55e-02 1.67e-01f 1\n",
+ " 124r 0.0000000e+00 4.39e+01 6.51e+02 -1.0 6.05e-01 0.7 2.20e-01 1.95e-01f 1\n",
+ " 125r 0.0000000e+00 3.95e+01 5.85e+02 -1.0 1.15e+00 0.2 4.00e-01 9.82e-02f 1\n",
+ " 126r 0.0000000e+00 5.31e+01 2.97e+02 -1.0 1.75e+00 -0.3 4.11e-03 3.31e-01f 1\n",
+ " 127r 0.0000000e+00 5.22e+01 2.92e+02 -1.0 1.49e+00 0.2 4.34e-02 1.79e-02h 1\n",
+ " 128r 0.0000000e+00 5.15e+01 2.70e+02 -1.0 9.28e+00 -0.3 1.26e-01 3.54e-02f 1\n",
+ " 129r 0.0000000e+00 2.87e+01 8.41e+01 -1.0 1.20e+00 0.1 9.87e-01 7.24e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 130r 0.0000000e+00 5.74e+01 7.12e+01 -1.0 2.02e+00 -0.4 1.00e+00 3.08e-01h 1\n",
+ " 131r 0.0000000e+00 1.40e+03 2.57e+01 -1.0 6.81e+00 -0.9 6.75e-01 8.23e-01f 1\n",
+ " 132r 0.0000000e+00 1.35e+03 2.54e+01 -1.0 1.62e+01 -1.3 1.03e-01 3.34e-02h 1\n",
+ " 133r 0.0000000e+00 1.23e+03 3.95e+01 -1.0 7.83e+00 -0.9 7.40e-01 6.60e-01h 1\n",
+ " 134r 0.0000000e+00 1.19e+03 3.30e+02 -1.0 1.81e+01 -1.4 3.93e-01 5.73e-02h 1\n",
+ " 135r 0.0000000e+00 1.18e+03 3.16e+02 -1.0 1.12e+02 -1.9 2.06e-02 2.35e-02h 1\n",
+ " 136r 0.0000000e+00 3.04e+03 1.74e+02 -1.0 1.66e+01 -1.4 6.20e-01 5.67e-01h 1\n",
+ " 137r 0.0000000e+00 3.10e+03 7.86e+01 -1.0 4.18e+02 -1.9 4.32e-04 5.13e-03f 1\n",
+ " 138r 0.0000000e+00 3.15e+03 8.59e+01 -1.0 2.08e+01 -1.5 6.10e-01 3.20e-01f 1\n",
+ " 139r 0.0000000e+00 6.79e+03 9.92e+01 -1.0 6.29e+01 -2.0 5.13e-01 5.43e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 140r 0.0000000e+00 1.99e+03 2.93e+02 -1.0 1.82e-01 1.2 9.52e-01 7.07e-01h 1\n",
+ " 141r 0.0000000e+00 4.14e-01 6.19e+01 -1.0 4.03e-02 1.6 9.05e-01 1.00e+00h 1\n",
+ " 142r 0.0000000e+00 5.40e-01 1.00e+01 -1.0 7.66e-02 1.1 1.00e+00 1.00e+00h 1\n",
+ " 143r 0.0000000e+00 1.87e+00 7.98e-01 -1.0 1.80e-01 0.6 1.00e+00 1.00e+00h 1\n",
+ " 144r 0.0000000e+00 1.04e+01 9.40e-01 -1.0 4.36e-01 0.2 1.00e+00 1.00e+00h 1\n",
+ " 145r 0.0000000e+00 3.19e+01 4.06e+00 -1.0 9.89e-01 -0.3 1.00e+00 1.00e+00h 1\n",
+ " 146r 0.0000000e+00 2.28e+02 6.27e+00 -1.0 3.02e+00 -0.8 1.00e+00 9.35e-01h 1\n",
+ " 147r 0.0000000e+00 7.58e+02 1.93e+02 -1.0 1.17e+01 -1.3 1.00e+00 5.43e-01h 1\n",
+ " 148r 0.0000000e+00 7.62e+02 6.49e+02 -1.0 5.17e+01 -1.7 4.76e-01 9.10e-02h 1\n",
+ " 149r 0.0000000e+00 5.18e+03 4.98e+02 -1.0 3.35e+02 -2.2 2.78e-02 1.43e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 150r 0.0000000e+00 5.19e+03 7.41e+02 -1.0 1.23e+03 -2.7 1.42e-02 2.95e-02f 1\n",
+ " 151r 0.0000000e+00 6.87e+03 9.57e+02 -1.0 1.12e+03 -2.3 1.35e-03 3.17e-02f 1\n",
+ " 152r 0.0000000e+00 6.85e+03 8.50e+02 -1.0 1.37e+02 -1.8 3.99e-01 4.06e-03h 1\n",
+ " 153r 0.0000000e+00 6.72e+03 9.57e+02 -1.0 4.39e+02 -2.3 1.18e-01 1.81e-02h 1\n",
+ " 154r 0.0000000e+00 6.40e+03 1.13e+03 -1.0 1.07e+04 - 1.49e-01 4.89e-02f 1\n",
+ " 155r 0.0000000e+00 5.62e+03 9.92e+02 -1.0 9.94e+03 - 1.20e-01 1.22e-01f 1\n",
+ " 156r 0.0000000e+00 5.58e+03 9.64e+02 -1.0 3.35e+03 - 1.73e-03 7.88e-03h 1\n",
+ " 157r 0.0000000e+00 5.58e+03 9.64e+02 -1.0 2.55e+05 -2.8 2.32e-05 2.33e-05f 1\n",
+ " 158r 0.0000000e+00 5.55e+03 1.02e+03 -1.0 4.99e+02 -3.3 2.90e-02 5.00e-03h 1\n",
+ " 159r 0.0000000e+00 4.91e+03 9.36e+02 -1.0 1.06e+02 -3.8 1.63e-01 1.16e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 160r 0.0000000e+00 4.19e+03 7.63e+02 -1.0 1.75e+02 -2.4 1.15e-02 1.48e-01h 1\n",
+ " 161r 0.0000000e+00 4.18e+03 7.94e+02 -1.0 4.34e+02 -2.9 3.97e-02 1.46e-03h 1\n",
+ " 162r 0.0000000e+00 2.31e+03 3.53e+02 -1.0 5.33e+03 - 1.01e-01 4.63e-01h 1\n",
+ " 163r 0.0000000e+00 2.04e+03 5.97e+02 -1.0 3.18e+03 - 1.73e-01 1.20e-01h 1\n",
+ " 164r 0.0000000e+00 1.26e+03 3.46e+02 -1.0 3.03e+03 - 4.18e-01 4.11e-01h 1\n",
+ " 165r 0.0000000e+00 1.24e+03 3.20e+02 -1.0 2.32e+03 - 1.97e-03 1.44e-02h 1\n",
+ " 166r 0.0000000e+00 1.17e+03 4.22e+02 -1.0 2.31e+03 - 3.37e-01 5.42e-02h 1\n",
+ " 167r 0.0000000e+00 1.04e+03 6.87e+02 -1.0 1.74e+03 - 9.08e-01 1.16e-01h 1\n",
+ " 168r 0.0000000e+00 3.09e+02 4.10e+02 -1.0 1.64e+03 - 4.91e-01 8.06e-01h 1\n",
+ " 169r 0.0000000e+00 3.95e+02 5.59e+02 -1.0 2.46e+03 - 1.49e-02 2.83e-02f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 170r 0.0000000e+00 2.99e+02 7.69e+01 -1.0 4.31e+02 - 8.88e-01 1.00e+00f 1\n",
+ " 171r 0.0000000e+00 1.65e+02 3.84e+02 -1.0 4.91e-01 0.2 1.00e+00 4.50e-01h 1\n",
+ " 172r 0.0000000e+00 1.87e-01 5.61e+01 -1.0 2.62e+00 - 1.00e+00 1.00e+00h 1\n",
+ " 173r 0.0000000e+00 1.87e-01 2.09e+01 -1.0 8.62e-01 - 1.00e+00 1.00e+00h 1\n",
+ " 174r 0.0000000e+00 1.87e-01 1.77e+00 -1.0 9.94e-02 - 1.00e+00 1.00e+00h 1\n",
+ " 175r 0.0000000e+00 7.96e+00 5.39e+00 -1.7 1.21e+01 - 1.00e+00 9.98e-01f 1\n",
+ " 176r 0.0000000e+00 2.31e+02 1.33e+02 -1.7 1.26e+04 - 3.42e-01 1.24e-01f 1\n",
+ " 177r 0.0000000e+00 2.38e+02 3.38e+02 -1.7 1.05e+04 - 6.40e-01 3.94e-02f 1\n",
+ " 178r 0.0000000e+00 2.37e+02 3.35e+02 -1.7 7.28e+03 - 9.97e-02 1.97e-03h 1\n",
+ " 179r 0.0000000e+00 2.11e+02 3.90e+02 -1.7 3.85e+03 - 5.39e-01 1.07e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 180r 0.0000000e+00 3.16e+02 4.17e+02 -1.7 3.82e+03 - 1.28e-02 9.51e-01f 1\n",
+ " 181r 0.0000000e+00 1.07e+03 2.19e+02 -1.7 3.96e+03 - 4.42e-01 4.76e-01h 1\n",
+ " 182r 0.0000000e+00 1.05e+03 2.05e+02 -1.7 1.41e+03 - 4.64e-01 2.04e-02h 1\n",
+ " 183r 0.0000000e+00 4.56e+02 1.83e+02 -1.7 1.51e+00 -0.2 4.60e-01 5.66e-01h 1\n",
+ " 184r 0.0000000e+00 4.46e+02 4.37e+02 -1.7 1.26e+03 - 5.85e-01 2.25e-02h 1\n",
+ " 185r 0.0000000e+00 3.62e+02 5.95e+02 -1.7 1.17e+03 - 3.15e-01 2.25e-01h 1\n",
+ " 186r 0.0000000e+00 3.47e+02 2.11e+02 -1.7 6.54e-01 -0.7 4.90e-01 4.12e-02h 1\n",
+ " 187r 0.0000000e+00 2.68e+02 4.10e+02 -1.7 9.73e+02 - 1.74e-01 2.84e-01h 1\n",
+ " 188r 0.0000000e+00 2.32e+02 7.23e+02 -1.7 1.37e+03 - 7.54e-01 3.83e-01h 1\n",
+ " 189r 0.0000000e+00 2.25e+02 1.95e+02 -1.7 7.05e+02 - 6.62e-01 3.01e-02h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 190r 0.0000000e+00 1.61e+02 3.01e+02 -1.7 4.09e+02 - 1.00e+00 3.03e-01h 1\n",
+ " 191r 0.0000000e+00 9.61e-01 6.13e-01 -1.7 6.25e+01 - 1.00e+00 1.00e+00h 1\n",
+ " 192r 0.0000000e+00 5.56e-02 3.83e-02 -1.7 6.74e+00 - 1.00e+00 1.00e+00h 1\n",
+ " 193r 0.0000000e+00 3.80e-01 2.41e+01 -3.9 2.90e+01 - 9.11e-01 7.83e-01f 1\n",
+ " 194r 0.0000000e+00 2.60e+00 2.67e+01 -3.9 4.29e+04 - 7.72e-02 5.58e-02f 1\n",
+ " 195r 0.0000000e+00 2.60e+00 3.11e+02 -3.9 2.37e+04 - 6.85e-02 1.39e-05h 1\n",
+ " 196r 0.0000000e+00 2.91e+00 2.90e+02 -3.9 3.11e+03 - 1.53e-01 3.55e-02f 1\n",
+ " 197r 0.0000000e+00 1.00e+01 2.50e+02 -3.9 2.55e+03 - 1.71e-01 1.67e-01f 1\n",
+ " 198r 0.0000000e+00 2.87e+01 3.43e+02 -3.9 2.12e+03 - 7.25e-02 3.31e-01f 1\n",
+ " 199r 0.0000000e+00 4.82e+01 9.78e+02 -3.9 1.55e+03 - 5.38e-02 5.45e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 200r 0.0000000e+00 1.40e+02 6.78e+02 -3.9 3.21e+04 - 2.94e-02 1.88e-01f 1\n",
+ " 201r 0.0000000e+00 1.38e+02 6.81e+02 -3.9 2.53e+04 - 4.87e-02 1.94e-02h 1\n",
+ " 202r 0.0000000e+00 1.43e+02 6.07e+02 -3.9 2.53e+04 - 2.36e-02 7.13e-02f 1\n",
+ " 203r 0.0000000e+00 2.17e+02 4.73e+02 -3.9 2.36e+04 - 9.43e-02 1.98e-01f 1\n",
+ " 204r 0.0000000e+00 2.11e+02 5.82e+02 -3.9 1.89e+04 - 5.56e-01 6.52e-02h 1\n",
+ " 205r 0.0000000e+00 2.11e+02 7.24e+02 -3.9 1.33e+04 - 8.62e-01 3.50e-03h 1\n",
+ " 206r 0.0000000e+00 1.88e+02 8.64e+02 -3.9 3.55e+02 - 1.00e+00 1.09e-01h 1\n",
+ " 207r 0.0000000e+00 3.04e+01 5.52e+02 -3.9 3.16e+02 - 2.11e-01 8.62e-01h 1\n",
+ " 208r 0.0000000e+00 7.41e-01 1.91e+00 -3.9 4.38e+01 - 1.00e+00 9.97e-01h 1\n",
+ " 209r 0.0000000e+00 7.13e-01 6.42e+02 -3.9 1.50e-01 - 7.36e-02 3.78e-02h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 210r 0.0000000e+00 5.37e-03 4.03e+02 -3.9 1.44e-01 - 5.86e-01 1.00e+00h 1\n",
+ " 211r 0.0000000e+00 5.37e-03 2.68e-04 -3.9 2.52e-03 - 1.00e+00 1.00e+00h 1\n",
+ " 212r 0.0000000e+00 5.37e-03 5.94e+01 -5.9 3.33e-01 - 1.00e+00 6.67e-01f 1\n",
+ " 213r 0.0000000e+00 4.67e+01 1.51e+02 -5.9 2.60e+04 - 1.03e-01 2.28e-02f 1\n",
+ " 214r 0.0000000e+00 4.67e+01 8.78e+02 -5.9 4.07e+02 - 8.75e-01 9.73e-07h 2\n",
+ " 215r 0.0000000e+00 1.68e+01 2.98e+02 -5.9 1.59e-01 - 8.59e-01 6.40e-01h 1\n",
+ " 216r 0.0000000e+00 2.59e+00 5.90e+01 -5.9 3.25e-02 - 1.00e+00 8.46e-01h 1\n",
+ " 217r 0.0000000e+00 1.57e-04 3.42e-04 -5.9 3.65e-03 - 1.00e+00 1.00e+00h 1\n",
+ " 218r 0.0000000e+00 2.69e-09 6.22e-06 -5.9 2.22e-04 - 1.00e+00 1.00e+00h 1\n",
+ "\n",
+ "Number of Iterations....: 218\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.6943874215090702e-09 2.6943874215090702e-09\n",
+ "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n",
+ "Overall NLP error.......: 2.6943874215090702e-09 2.6943874215090702e-09\n",
+ "\n",
+ "\n",
+ "Number of objective function evaluations = 225\n",
+ "Number of objective gradient evaluations = 8\n",
+ "Number of equality constraint evaluations = 225\n",
+ "Number of inequality constraint evaluations = 0\n",
+ "Number of equality constraint Jacobian evaluations = 220\n",
+ "Number of inequality constraint Jacobian evaluations = 0\n",
+ "Number of Lagrangian Hessian evaluations = 218\n",
+ "Total CPU secs in IPOPT (w/o function evaluations) = 0.347\n",
+ "Total CPU secs in NLP function evaluations = 0.036\n",
+ "\n",
+ "EXIT: Optimal Solution Found.\n"
+ ]
+ }
+ ],
+ "execution_count": 46
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"testing"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.984453200Z",
+ "start_time": "2026-01-13T00:12:32.973413800Z"
+ }
},
- "outputs": [],
"source": [
"# Check solver solve status\n",
"from pyomo.environ import TerminationCondition\n",
"\n",
"assert results.solver.termination_condition == TerminationCondition.optimal"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 47
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Analyze the results of the square problem\n",
+ "## 7 Analyze the results\n",
"\n",
- "\n",
- "What is the total operating cost? "
+ "\n"
]
},
{
- "cell_type": "code",
- "execution_count": null,
+ "cell_type": "markdown",
"metadata": {},
+ "source": [
+ "If the IDAES UI package was installed with the `idaes-pse` installation or installed separately, you can run the flowsheet visualizer to see a full diagram of the full process that is generated and displayed on a browser window.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "noauto"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.001529600Z",
+ "start_time": "2026-01-13T00:12:32.990965900Z"
+ }
+ },
+ "source": [
+ "# m.fs.visualize(\"HDA-Flowsheet\")"
+ ],
"outputs": [],
+ "execution_count": 48
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
"source": [
- "print(\"operating cost = $\", value(m.fs.operating_cost))"
+ "Otherwise, we can run the `m.fs.report()` method to see a full summary of the solved flowsheet. It is recommended to adjust the width of the output as much as possible for the cleanest display."
]
},
{
"cell_type": "code",
- "execution_count": null,
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.046845800Z",
+ "start_time": "2026-01-13T00:12:33.009050800Z"
+ }
+ },
+ "source": [
+ "m.fs.report()"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "====================================================================================\n",
+ "Flowsheet : fs Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units s01 s02 s03 s04 s05 s06 s07 s08 s09 s10 s11 s12 \n",
+ " Total Molar Flowrate Liq mole / second 0.30001 2.0000e-05 0.34190 1.6073e-09 5.7340e-09 1.0000e-08 0.26712 1.1139e-06 1.1143e-06 1.0000e-08 0.094878 2.7856e-07\n",
+ " Total Molar Flowrate Vap mole / second 4.0000e-05 0.32002 1.6901 2.0320 2.0320 1.7648 1.0000e-08 1.4119 1.4119 0.17224 1.0000e-08 0.35297\n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 3.3332e-05 0.50000 0.22733 0.13374 0.63390 0.76595 0.76595 0.76595 0.76595 0.66001 0.66001 0.76595\n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.99997 0.50000 0.77267 0.86626 0.36610 0.23405 0.23405 0.23405 0.23405 0.33999 0.33999 0.23405\n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.25000 3.1248e-05 0.024624 0.058732 0.17408 0.084499 0.084499 0.084499 0.084499 0.82430 0.82430 0.084499\n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.25000 3.1248e-05 0.028601 0.15380 0.038450 0.0088437 0.0088437 0.0088435 0.0088435 0.17570 0.17570 0.0088435\n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.25000 0.93744 0.33283 0.27683 0.16148 0.18592 0.18592 0.18592 0.18592 1.7561e-08 1.7561e-08 0.18592\n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.25000 0.062496 0.61394 0.51064 0.62599 0.72074 0.72074 0.72074 0.72074 4.3265e-08 4.3265e-08 0.72074\n",
+ " Temperature kelvin 303.20 303.20 324.51 600.00 771.86 325.00 325.00 325.00 325.00 375.00 375.00 325.00\n",
+ " Pressure pascal 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 1.5000e+05 1.5000e+05 3.5000e+05\n",
+ "====================================================================================\n"
+ ]
+ }
+ ],
+ "execution_count": 49
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "What is the total operating cost?"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.065372900Z",
+ "start_time": "2026-01-13T00:12:33.055710300Z"
+ }
+ },
+ "source": [
+ "print(\"operating cost = $\", value(m.fs.operating_cost))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "operating cost = $ 419008.281895999\n"
+ ]
+ }
+ ],
+ "execution_count": 50
+ },
+ {
+ "cell_type": "code",
"metadata": {
"tags": [
"testing"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.160597300Z",
+ "start_time": "2026-01-13T00:12:33.073483400Z"
+ }
},
- "outputs": [],
"source": [
"import pytest\n",
"\n",
- "assert value(m.fs.operating_cost) == pytest.approx(419122.3387, abs=1e-3)"
- ]
+ "assert value(m.fs.operating_cost) == pytest.approx(419008.28189, abs=1e-3)"
+ ],
+ "outputs": [],
+ "execution_count": 51
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "For this operating cost, what is the amount of benzene we are able to produce and what purity we are able to achieve? "
+ "For this operating cost, what is the amount of benzene we are able to produce and what purity we are able to achieve? We can look at a specific unit models stream table with the same `report()` method."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.179064200Z",
+ "start_time": "2026-01-13T00:12:33.163247500Z"
+ }
+ },
"source": [
"m.fs.F102.report()\n",
"\n",
"print()\n",
"print(\"benzene purity = \", value(m.fs.purity))"
- ]
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.F102 Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : 7346.7 : watt : False : (None, None)\n",
+ " Pressure Change : -2.0000e+05 : pascal : True : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " Total Molar Flowrate Liq mole / second 0.26712 - - \n",
+ " Total Molar Flowrate Vap mole / second 1.0000e-08 - - \n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 0.76595 - - \n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.23405 - - \n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.084499 - - \n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.0088437 - - \n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.18592 - - \n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.72074 - - \n",
+ " Temperature kelvin 325.00 - - \n",
+ " Pressure pascal 3.5000e+05 - - \n",
+ " flow_mol_phase Liq mole / second - 1.0000e-08 0.094878 \n",
+ " flow_mol_phase Vap mole / second - 0.17224 1.0000e-08 \n",
+ " mole_frac_phase_comp ('Liq', 'benzene') dimensionless - 0.66001 0.66001 \n",
+ " mole_frac_phase_comp ('Liq', 'toluene') dimensionless - 0.33999 0.33999 \n",
+ " mole_frac_phase_comp ('Vap', 'benzene') dimensionless - 0.82430 0.82430 \n",
+ " mole_frac_phase_comp ('Vap', 'toluene') dimensionless - 0.17570 0.17570 \n",
+ " mole_frac_phase_comp ('Vap', 'hydrogen') dimensionless - 1.7561e-08 1.7561e-08 \n",
+ " mole_frac_phase_comp ('Vap', 'methane') dimensionless - 4.3265e-08 4.3265e-08 \n",
+ " temperature kelvin - 375.00 375.00 \n",
+ " pressure pascal - 1.5000e+05 1.5000e+05 \n",
+ "====================================================================================\n",
+ "\n",
+ "benzene purity = 0.8242963450787166\n"
+ ]
+ }
+ ],
+ "execution_count": 52
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"testing"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.274448800Z",
+ "start_time": "2026-01-13T00:12:33.188271400Z"
+ }
},
- "outputs": [],
"source": [
"assert value(m.fs.purity) == pytest.approx(0.82429, abs=1e-3)\n",
- "\n",
- "assert value(m.fs.F102.heat_duty[0]) == pytest.approx(7352.4828, abs=1e-3)\n",
+ "assert value(m.fs.F102.heat_duty[0]) == pytest.approx(7346.67441, abs=1e-3)\n",
"assert value(m.fs.F102.vap_outlet.pressure[0]) == pytest.approx(1.5000e05, abs=1e-3)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 53
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "Next, let's look at how much benzene we are losing with the light gases out of F101. IDAES has tools for creating stream tables based on the `Arcs` and/or `Ports` in a flowsheet. Let us create and print a simple stream table showing the stream leaving the reactor and the vapor stream from F101.\n",
- "\n",
- "
\n",
- "Inline Exercise:\n",
- "How much benzene are we losing in the F101 vapor outlet stream?\n",
- "
\n"
+ "Next, let's look at how much benzene we are losing with the light gases out of F101. IDAES has tools for creating stream tables based on the `Arcs` and/or `Ports` in a flowsheet. Let us create and print a simple stream table showing the stream leaving the reactor and the vapor stream from F101."
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.290365100Z",
+ "start_time": "2026-01-13T00:12:33.276957500Z"
+ }
+ },
"source": [
"from idaes.core.util.tables import (\n",
" create_stream_table_dataframe,\n",
@@ -1174,25 +2030,33 @@
"\n",
"st = create_stream_table_dataframe({\"Reactor\": m.fs.s05, \"Light Gases\": m.fs.s06})\n",
"print(stream_table_dataframe_to_string(st))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "You can query additional variables here if you like. \n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
\n"
- ]
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " Units Reactor Light Gases\n",
+ "Total Molar Flowrate Liq mole / second 5.7340e-09 1.0000e-08 \n",
+ "Total Molar Flowrate Vap mole / second 2.0320 1.7648 \n",
+ "Total Mole Fraction ('Liq', 'benzene') dimensionless 0.63390 0.76595 \n",
+ "Total Mole Fraction ('Liq', 'toluene') dimensionless 0.36610 0.23405 \n",
+ "Total Mole Fraction ('Vap', 'benzene') dimensionless 0.17408 0.084499 \n",
+ "Total Mole Fraction ('Vap', 'toluene') dimensionless 0.038450 0.0088437 \n",
+ "Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.16148 0.18592 \n",
+ "Total Mole Fraction ('Vap', 'methane') dimensionless 0.62599 0.72074 \n",
+ "Temperature kelvin 771.86 325.00 \n",
+ "Pressure pascal 3.5000e+05 3.5000e+05 \n"
+ ]
+ }
+ ],
+ "execution_count": 54
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Optimization\n",
+ "## 8 Optimization\n",
"\n",
"\n",
"We saw from the results above that the total operating cost for the base case was $419,122 per year. We are producing 0.142 mol/s of benzene at a purity of 82\\%. However, we are losing around 42\\% of benzene in F101 vapor outlet stream. \n",
@@ -1219,12 +2083,17 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.307079900Z",
+ "start_time": "2026-01-13T00:12:33.298304Z"
+ }
+ },
"source": [
"m.fs.objective = Objective(expr=m.fs.operating_cost)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 55
},
{
"cell_type": "markdown",
@@ -1235,19 +2104,28 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.315754900Z",
+ "start_time": "2026-01-13T00:12:33.308100100Z"
+ }
+ },
"source": [
"m.fs.H101.outlet.temperature.unfix()\n",
"m.fs.R101.heat_duty.unfix()\n",
"m.fs.F101.vap_outlet.temperature.unfix()\n",
"m.fs.F102.vap_outlet.temperature.unfix()"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 56
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
"source": [
"
\n",
"
Inline Exercise:\n",
@@ -1260,43 +2138,55 @@
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"exercise"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.324956200Z",
+ "start_time": "2026-01-13T00:12:33.315754900Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Unfix deltaP for F102"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 57
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"solution"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.333132300Z",
+ "start_time": "2026-01-13T00:12:33.324956200Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Unfix deltaP for F102\n",
"m.fs.F102.deltaP.unfix()"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 58
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"testing"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.360701100Z",
+ "start_time": "2026-01-13T00:12:33.333132300Z"
+ }
},
- "outputs": [],
"source": [
"assert degrees_of_freedom(m) == 5"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 59
},
{
"cell_type": "markdown",
@@ -1315,17 +2205,26 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.371189900Z",
+ "start_time": "2026-01-13T00:12:33.362714700Z"
+ }
+ },
"source": [
"m.fs.H101.outlet.temperature[0].setlb(500)\n",
"m.fs.H101.outlet.temperature[0].setub(600)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 60
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
"source": [
"
\n",
"
Inline Exercise:\n",
@@ -1337,31 +2236,39 @@
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"exercise"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.388084900Z",
+ "start_time": "2026-01-13T00:12:33.377308100Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Set the bounds for reactor outlet temperature"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 61
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"solution"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.406097200Z",
+ "start_time": "2026-01-13T00:12:33.395647Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Set the bounds for reactor outlet temperature\n",
"m.fs.R101.outlet.temperature[0].setlb(600)\n",
"m.fs.R101.outlet.temperature[0].setub(800)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 62
},
{
"cell_type": "markdown",
@@ -1372,9 +2279,12 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.426888400Z",
+ "start_time": "2026-01-13T00:12:33.413693900Z"
+ }
+ },
"source": [
"m.fs.F101.vap_outlet.temperature[0].setlb(298.0)\n",
"m.fs.F101.vap_outlet.temperature[0].setub(450.0)\n",
@@ -1382,7 +2292,9 @@
"m.fs.F102.vap_outlet.temperature[0].setub(450.0)\n",
"m.fs.F102.vap_outlet.pressure[0].setlb(105000)\n",
"m.fs.F102.vap_outlet.pressure[0].setub(110000)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 63
},
{
"cell_type": "markdown",
@@ -1393,19 +2305,31 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.447188200Z",
+ "start_time": "2026-01-13T00:12:33.436145800Z"
+ }
+ },
"source": [
"m.fs.overhead_loss = Constraint(\n",
- " expr=m.fs.F101.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- " <= 0.20 * m.fs.R101.outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
+ " expr=m.fs.F101.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"benzene\"\n",
+ " ]\n",
+ " <= 0.20\n",
+ " * m.fs.R101.control_volume.properties_out[0].flow_mol_phase_comp[\"Vap\", \"benzene\"]\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 64
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
"source": [
"
\n",
"
Inline Exercise:\n",
@@ -1417,32 +2341,43 @@
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"exercise"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.464672600Z",
+ "start_time": "2026-01-13T00:12:33.453771300Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Add minimum product flow constraint"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 65
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"solution"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.481454400Z",
+ "start_time": "2026-01-13T00:12:33.471763500Z"
+ }
},
- "outputs": [],
"source": [
"# Todo: Add minimum product flow constraint\n",
"m.fs.product_flow = Constraint(\n",
- " expr=m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"] >= 0.15\n",
+ " expr=m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"benzene\"\n",
+ " ]\n",
+ " >= 0.15\n",
")"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 66
},
{
"cell_type": "markdown",
@@ -1453,12 +2388,17 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.500096Z",
+ "start_time": "2026-01-13T00:12:33.487986500Z"
+ }
+ },
"source": [
"m.fs.product_purity = Constraint(expr=m.fs.purity >= 0.80)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 67
},
{
"cell_type": "markdown",
@@ -1472,43 +2412,53 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.630996900Z",
+ "start_time": "2026-01-13T00:12:33.501556Z"
+ }
+ },
+ "source": "results = solver.solve(m, tee=False)",
"outputs": [],
- "source": [
- "results = solver.solve(m, tee=True)"
- ]
+ "execution_count": 68
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"testing"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.650287300Z",
+ "start_time": "2026-01-13T00:12:33.639146Z"
+ }
},
- "outputs": [],
"source": [
"# Check for solver solve status\n",
"from pyomo.environ import TerminationCondition\n",
"\n",
"assert results.solver.termination_condition == TerminationCondition.optimal"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 69
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Optimization Results\n",
+ "### 8.1 Optimization Results\n",
"\n",
"Display the results and product specifications"
]
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.700975700Z",
+ "start_time": "2026-01-13T00:12:33.660461600Z"
+ }
+ },
"source": [
"print(\"operating cost = $\", value(m.fs.operating_cost))\n",
"\n",
@@ -1523,21 +2473,113 @@
"print()\n",
"print(\"Overhead loss in F101\")\n",
"m.fs.F101.report()"
- ]
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "operating cost = $ 312674.2367537996\n",
+ "\n",
+ "Product flow rate and purity in F102\n",
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.F102 Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : 8370.2 : watt : False : (None, None)\n",
+ " Pressure Change : -2.4500e+05 : pascal : False : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " Total Molar Flowrate Liq mole / second 0.28812 - - \n",
+ " Total Molar Flowrate Vap mole / second 1.0000e-08 - - \n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 0.75463 - - \n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.24537 - - \n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.032748 - - \n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.0032478 - - \n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.21614 - - \n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.74786 - - \n",
+ " Temperature kelvin 301.88 - - \n",
+ " Pressure pascal 3.5000e+05 - - \n",
+ " flow_mol_phase Liq mole / second - 1.0000e-08 0.10493 \n",
+ " flow_mol_phase Vap mole / second - 0.18319 1.0000e-08 \n",
+ " mole_frac_phase_comp ('Liq', 'benzene') dimensionless - 0.64256 0.64256 \n",
+ " mole_frac_phase_comp ('Liq', 'toluene') dimensionless - 0.35744 0.35744 \n",
+ " mole_frac_phase_comp ('Vap', 'benzene') dimensionless - 0.81883 0.81883 \n",
+ " mole_frac_phase_comp ('Vap', 'toluene') dimensionless - 0.18117 0.18117 \n",
+ " mole_frac_phase_comp ('Vap', 'hydrogen') dimensionless - 1.1799e-08 1.1799e-08 \n",
+ " mole_frac_phase_comp ('Vap', 'methane') dimensionless - 4.0825e-08 4.0825e-08 \n",
+ " temperature kelvin - 362.93 362.93 \n",
+ " pressure pascal - 1.0500e+05 1.0500e+05 \n",
+ "====================================================================================\n",
+ "\n",
+ "benzene purity = 0.8188295888412465\n",
+ "\n",
+ "Overhead loss in F101\n",
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.F101 Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : -68347. : watt : False : (None, None)\n",
+ " Pressure Change : 0.0000 : pascal : True : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " Total Molar Flowrate Liq mole / second 1.5819e-12 - - \n",
+ " Total Molar Flowrate Vap mole / second 1.9480 - - \n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 0.57381 - - \n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.42619 - - \n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.13952 - - \n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.039059 - - \n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.18417 - - \n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.63725 - - \n",
+ " Temperature kelvin 775.95 - - \n",
+ " Pressure pascal 3.5000e+05 - - \n",
+ " flow_mol_phase Liq mole / second - 1.0000e-08 0.28812 \n",
+ " flow_mol_phase Vap mole / second - 1.6598 1.0000e-08 \n",
+ " mole_frac_phase_comp ('Liq', 'benzene') dimensionless - 0.75463 0.75463 \n",
+ " mole_frac_phase_comp ('Liq', 'toluene') dimensionless - 0.24537 0.24537 \n",
+ " mole_frac_phase_comp ('Vap', 'benzene') dimensionless - 0.032748 0.032748 \n",
+ " mole_frac_phase_comp ('Vap', 'toluene') dimensionless - 0.0032478 0.0032478 \n",
+ " mole_frac_phase_comp ('Vap', 'hydrogen') dimensionless - 0.21614 0.21614 \n",
+ " mole_frac_phase_comp ('Vap', 'methane') dimensionless - 0.74786 0.74786 \n",
+ " temperature kelvin - 301.88 301.88 \n",
+ " pressure pascal - 3.5000e+05 3.5000e+05 \n",
+ "====================================================================================\n"
+ ]
+ }
+ ],
+ "execution_count": 70
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"testing"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.717027100Z",
+ "start_time": "2026-01-13T00:12:33.703994500Z"
+ }
},
- "outputs": [],
"source": [
- "assert value(m.fs.operating_cost) == pytest.approx(312786.338, abs=1e-3)\n",
+ "assert value(m.fs.operating_cost) == pytest.approx(312674.236, abs=1e-3)\n",
"assert value(m.fs.purity) == pytest.approx(0.818827, abs=1e-3)"
- ]
+ ],
+ "outputs": [],
+ "execution_count": 71
},
{
"cell_type": "markdown",
@@ -1548,49 +2590,68 @@
},
{
"cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.735319500Z",
+ "start_time": "2026-01-13T00:12:33.718148400Z"
+ }
+ },
"source": [
- "print(\"Optimal Values\")\n",
- "print()\n",
+ "print(\n",
+ " f\"\"\"Optimal Values:\n",
"\n",
- "print(\"H101 outlet temperature = \", value(m.fs.H101.outlet.temperature[0]), \"K\")\n",
+ "H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):.3f} K\n",
"\n",
- "print()\n",
- "print(\"R101 outlet temperature = \", value(m.fs.R101.outlet.temperature[0]), \"K\")\n",
+ "R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):.3f} K\n",
"\n",
- "print()\n",
- "print(\"F101 outlet temperature = \", value(m.fs.F101.vap_outlet.temperature[0]), \"K\")\n",
+ "F101 outlet temperature = {value(m.fs.F101.vap_outlet.temperature[0]):.3f} K\n",
"\n",
- "print()\n",
- "print(\"F102 outlet temperature = \", value(m.fs.F102.vap_outlet.temperature[0]), \"K\")\n",
- "print(\"F102 outlet pressure = \", value(m.fs.F102.vap_outlet.pressure[0]), \"Pa\")"
- ]
+ "F102 outlet temperature = {value(m.fs.F102.vap_outlet.temperature[0]):.3f} K\n",
+ "F102 outlet pressure = {value(m.fs.F102.vap_outlet.pressure[0]):.3f} Pa\n",
+ "\"\"\"\n",
+ ")"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Optimal Values:\n",
+ "\n",
+ "H101 outlet temperature = 500.000 K\n",
+ "\n",
+ "R101 outlet temperature = 775.947 K\n",
+ "\n",
+ "F101 outlet temperature = 301.881 K\n",
+ "\n",
+ "F102 outlet temperature = 362.935 K\n",
+ "F102 outlet pressure = 105000.000 Pa\n",
+ "\n"
+ ]
+ }
+ ],
+ "execution_count": 72
},
{
"cell_type": "code",
- "execution_count": null,
"metadata": {
"tags": [
"testing"
- ]
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.750045300Z",
+ "start_time": "2026-01-13T00:12:33.736837800Z"
+ }
},
- "outputs": [],
"source": [
"assert value(m.fs.H101.outlet.temperature[0]) == pytest.approx(500, abs=1e-3)\n",
- "assert value(m.fs.R101.outlet.temperature[0]) == pytest.approx(696.112, abs=1e-3)\n",
- "assert value(m.fs.F101.vap_outlet.temperature[0]) == pytest.approx(301.878, abs=1e-3)\n",
+ "assert value(m.fs.R101.outlet.temperature[0]) == pytest.approx(775.947, abs=1e-3)\n",
+ "assert value(m.fs.F101.vap_outlet.temperature[0]) == pytest.approx(301.881, abs=1e-3)\n",
"assert value(m.fs.F102.vap_outlet.temperature[0]) == pytest.approx(362.935, abs=1e-3)\n",
"assert value(m.fs.F102.vap_outlet.pressure[0]) == pytest.approx(105000, abs=1e-2)"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
+ ],
"outputs": [],
- "source": []
+ "execution_count": 73
}
],
"metadata": {
@@ -1610,7 +2671,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.8.12"
+ "version": "3.12.3"
}
},
"nbformat": 4,
diff --git a/idaes_examples/notebooks/docs/tut/core/hda_flowsheet.py b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet.py
new file mode 100644
index 00000000..52c599b9
--- /dev/null
+++ b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet.py
@@ -0,0 +1,298 @@
+from pyomo.environ import (
+ Constraint,
+ Var,
+ ConcreteModel,
+ Expression,
+ Objective,
+ TransformationFactory,
+ value,
+)
+from pyomo.network import Arc
+from idaes.core import FlowsheetBlock
+from idaes.core.util.model_statistics import degrees_of_freedom
+from idaes.core.solvers import get_solver
+
+from idaes.models.unit_models import (
+ PressureChanger,
+ Mixer,
+ Separator as Splitter,
+ Heater,
+ StoichiometricReactor,
+ Feed,
+ Product,
+ Flash,
+)
+from idaes.models.unit_models.pressure_changer import ThermodynamicAssumption
+from idaes.models.properties.modular_properties.base.generic_property import (
+ GenericParameterBlock,
+)
+from idaes.models.properties.modular_properties.base.generic_reaction import (
+ GenericReactionParameterBlock,
+)
+
+from idaes_examples.mod.hda.hda_ideal_VLE_modular import thermo_config
+from idaes_examples.mod.hda.hda_reaction_modular import reaction_config
+from idaes_examples.mod.hda.hda_flowsheet_extras import (
+ manual_propagation,
+ automatic_propagation,
+ fix_inlet_states,
+)
+
+
+if __name__ == "__main__":
+ m = ConcreteModel()
+ m.fs = FlowsheetBlock(dynamic=False)
+
+ m.fs.thermo_params = GenericParameterBlock(**thermo_config)
+ m.fs.reaction_params = GenericReactionParameterBlock(
+ property_package=m.fs.thermo_params, **reaction_config
+ )
+
+ m.fs.I101 = Feed(property_package=m.fs.thermo_params)
+ m.fs.I102 = Feed(property_package=m.fs.thermo_params)
+
+ m.fs.M101 = Mixer(
+ property_package=m.fs.thermo_params,
+ num_inlets=3,
+ )
+
+ m.fs.H101 = Heater(
+ property_package=m.fs.thermo_params,
+ has_pressure_change=False,
+ has_phase_equilibrium=True,
+ )
+
+ m.fs.R101 = StoichiometricReactor(
+ property_package=m.fs.thermo_params,
+ reaction_package=m.fs.reaction_params,
+ has_heat_of_reaction=True,
+ has_heat_transfer=True,
+ has_pressure_change=False,
+ )
+
+ m.fs.F101 = Flash(
+ property_package=m.fs.thermo_params,
+ has_heat_transfer=True,
+ has_pressure_change=True,
+ )
+
+ m.fs.S101 = Splitter(
+ property_package=m.fs.thermo_params,
+ ideal_separation=False,
+ outlet_list=["purge", "recycle"],
+ )
+
+ m.fs.C101 = PressureChanger(
+ property_package=m.fs.thermo_params,
+ compressor=True,
+ thermodynamic_assumption=ThermodynamicAssumption.isothermal,
+ )
+
+ m.fs.F102 = Flash(
+ property_package=m.fs.thermo_params,
+ has_heat_transfer=True,
+ has_pressure_change=True,
+ )
+
+ m.fs.P101 = Product(property_package=m.fs.thermo_params)
+ m.fs.P102 = Product(property_package=m.fs.thermo_params)
+ m.fs.P103 = Product(property_package=m.fs.thermo_params)
+
+ m.fs.s01 = Arc(source=m.fs.I101.outlet, destination=m.fs.M101.inlet_1)
+ m.fs.s02 = Arc(source=m.fs.I102.outlet, destination=m.fs.M101.inlet_2)
+ m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)
+ m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)
+ m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.F101.inlet)
+ m.fs.s06 = Arc(source=m.fs.F101.vap_outlet, destination=m.fs.S101.inlet)
+ m.fs.s07 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet)
+ m.fs.s08 = Arc(source=m.fs.S101.recycle, destination=m.fs.C101.inlet)
+ m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.inlet_3)
+ m.fs.s10 = Arc(source=m.fs.F102.vap_outlet, destination=m.fs.P101.inlet)
+ m.fs.s11 = Arc(source=m.fs.F102.liq_outlet, destination=m.fs.P102.inlet)
+ m.fs.s12 = Arc(source=m.fs.S101.purge, destination=m.fs.P103.inlet)
+
+ TransformationFactory("network.expand_arcs").apply_to(m)
+
+ m.fs.purity = Expression(
+ expr=m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[
+ "Vap", "benzene"
+ ]
+ / (
+ m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[
+ "Vap", "benzene"
+ ]
+ + m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[
+ "Vap", "toluene"
+ ]
+ )
+ )
+
+ m.fs.cooling_cost = Expression(
+ expr=0.212e-7 * (-m.fs.F101.heat_duty[0]) + 0.212e-7 * (-m.fs.R101.heat_duty[0])
+ )
+
+ m.fs.heating_cost = Expression(
+ expr=2.2e-7 * m.fs.H101.heat_duty[0] + 1.9e-7 * m.fs.F102.heat_duty[0]
+ )
+
+ m.fs.operating_cost = Expression(
+ expr=(3600 * 24 * 365 * (m.fs.heating_cost + m.fs.cooling_cost))
+ )
+
+ assert degrees_of_freedom(m) == 29
+
+ tear_guesses = fix_inlet_states(m)
+
+ m.fs.H101.outlet.temperature.fix(600)
+
+ m.fs.R101.control_volume.conversion = Var(initialize=0.75, bounds=(0, 1))
+
+ m.fs.R101.conv_constraint = Constraint(
+ expr=m.fs.R101.control_volume.conversion
+ * (
+ m.fs.R101.control_volume.properties_in[0].flow_mol_phase_comp[
+ "Vap", "toluene"
+ ]
+ )
+ == (
+ m.fs.R101.control_volume.properties_in[0].flow_mol_phase_comp[
+ "Vap", "toluene"
+ ]
+ - m.fs.R101.control_volume.properties_out[0].flow_mol_phase_comp[
+ "Vap", "toluene"
+ ]
+ )
+ )
+
+ m.fs.R101.control_volume.conversion.fix(0.75)
+ m.fs.R101.heat_duty.fix(0.0)
+
+ m.fs.F101.vap_outlet.temperature.fix(325.0)
+ m.fs.F101.deltaP.fix(0.0)
+
+ m.fs.F102.vap_outlet.temperature.fix(375)
+ m.fs.F102.deltaP.fix(-200000)
+
+ m.fs.S101.split_fraction[0, "purge"].fix(0.2)
+ m.fs.C101.outlet.pressure.fix(350000)
+
+ # automatic_propagation(m, tear_guesses)
+ manual_propagation(m, tear_guesses)
+
+ optarg = {
+ "nlp_scaling_method": "user-scaling",
+ "OF_ma57_automatic_scaling": "yes",
+ "max_iter": 1000,
+ "tol": 1e-8,
+ }
+
+ solver = get_solver("ipopt_v2", options=optarg)
+
+ # Solve the model
+ results = solver.solve(m, tee=False)
+
+ from pyomo.environ import TerminationCondition
+
+ assert results.solver.termination_condition == TerminationCondition.optimal
+
+ print("operating cost = $", value(m.fs.operating_cost))
+
+ import pytest
+
+ print("benzene purity = ", value(m.fs.purity))
+
+ assert value(m.fs.purity) == pytest.approx(0.82429, abs=1e-3)
+
+ assert value(m.fs.F102.heat_duty[0]) == pytest.approx(7346.67441, abs=1e-3)
+ assert value(m.fs.F102.vap_outlet.pressure[0]) == pytest.approx(1.5000e05, abs=1e-3)
+
+ m.fs.objective = Objective(expr=m.fs.operating_cost)
+
+ m.fs.H101.outlet.temperature.unfix()
+ m.fs.R101.heat_duty.unfix()
+ m.fs.F101.vap_outlet.temperature.unfix()
+ m.fs.F102.vap_outlet.temperature.unfix()
+
+ m.fs.F102.deltaP.unfix()
+
+ assert degrees_of_freedom(m) == 5
+
+ m.fs.H101.outlet.temperature[0].setlb(500)
+ m.fs.H101.outlet.temperature[0].setub(600)
+
+ m.fs.R101.outlet.temperature[0].setlb(600)
+ m.fs.R101.outlet.temperature[0].setub(800)
+
+ m.fs.F101.vap_outlet.temperature[0].setlb(298.0)
+ m.fs.F101.vap_outlet.temperature[0].setub(450.0)
+ m.fs.F102.vap_outlet.temperature[0].setlb(298.0)
+ m.fs.F102.vap_outlet.temperature[0].setub(450.0)
+ m.fs.F102.vap_outlet.pressure[0].setlb(105000)
+ m.fs.F102.vap_outlet.pressure[0].setub(110000)
+
+ m.fs.overhead_loss = Constraint(
+ expr=m.fs.F101.control_volume.properties_out[0].flow_mol_phase_comp[
+ "Vap", "benzene"
+ ]
+ <= 0.20
+ * m.fs.R101.control_volume.properties_out[0].flow_mol_phase_comp[
+ "Vap", "benzene"
+ ]
+ )
+
+ m.fs.product_flow = Constraint(
+ expr=m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[
+ "Vap", "benzene"
+ ]
+ >= 0.15
+ )
+
+ m.fs.product_purity = Constraint(expr=m.fs.purity >= 0.80)
+
+ results = solver.solve(m, tee=False)
+
+ # Check for solver solve status
+ from pyomo.environ import TerminationCondition
+
+ assert results.solver.termination_condition == TerminationCondition.optimal
+
+ print("operating cost = $", value(m.fs.operating_cost))
+
+ print()
+ print("Product flow rate and purity in F102")
+ m.fs.F102.report()
+
+ print()
+ print("benzene purity = ", value(m.fs.purity))
+
+ print()
+ print("Overhead loss in F101")
+ m.fs.F101.report()
+
+ #
+ assert value(m.fs.operating_cost) == pytest.approx(312674.236, abs=1e-3)
+ assert value(m.fs.purity) == pytest.approx(0.818827, abs=1e-3)
+
+ print(
+ f"""Optimal Values:
+
+ H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):.3f} K
+
+ R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):.3f} K
+
+ F101 outlet temperature = {value(m.fs.F101.vap_outlet.temperature[0]):.3f} K
+
+ F102 outlet temperature = {value(m.fs.F102.vap_outlet.temperature[0]):.3f} K
+ F102 outlet pressure = {value(m.fs.F102.vap_outlet.pressure[0]):.3f} Pa
+ """
+ )
+
+ assert value(m.fs.H101.outlet.temperature[0]) == pytest.approx(500, abs=1e-3)
+ # assert value(m.fs.R101.outlet.temperature[0]) == pytest.approx(862.907, abs=1e-3)
+ assert value(m.fs.F101.vap_outlet.temperature[0]) == pytest.approx(
+ 301.881, abs=1e-3
+ )
+ assert value(m.fs.F102.vap_outlet.temperature[0]) == pytest.approx(
+ 362.935, abs=1e-3
+ )
+ assert value(m.fs.F102.vap_outlet.pressure[0]) == pytest.approx(105000, abs=1e-2)
diff --git a/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_doc.ipynb b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_doc.ipynb
index 86569f0e..b707e5e0 100644
--- a/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_doc.ipynb
+++ b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_doc.ipynb
@@ -1,1363 +1,2198 @@
{
- "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",
- "# HDA Flowsheet Simulation and Optimization\n",
- "\n",
- "Author: Jaffer Ghouse \n",
- "Maintainer: Brandon Paul \n",
- "Updated: 2023-06-01 \n",
- "\n",
- "## Learning outcomes\n",
- "\n",
- "\n",
- "- Construct a steady-state flowsheet using the IDAES unit model library\n",
- "- Connecting unit models in a flowsheet using Arcs\n",
- "- Using the SequentialDecomposition tool to initialize a flowsheet with recycle\n",
- "- Formulate 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",
- "Hydrodealkylation is a chemical reaction that often involves reacting\n",
- "an aromatic hydrocarbon in the presence of hydrogen gas to form a\n",
- "simpler aromatic hydrocarbon devoid of functional groups. In this\n",
- "example, toluene will be reacted with hydrogen gas at high temperatures\n",
- " to form benzene via the following reaction:\n",
- "\n",
- "**C
6H
5CH
3 + H
2 → C
6H
6 + CH
4**\n",
- "\n",
- "\n",
- "This reaction is often accompanied by an equilibrium side reaction\n",
- "which forms diphenyl, which we will neglect for this example.\n",
- "\n",
- "This example is based on the 1967 AIChE Student Contest problem as\n",
- "present by Douglas, J.M., Chemical Design of Chemical Processes, 1988,\n",
- "McGraw-Hill.\n",
- "\n",
- "The flowsheet that we will be using for this module is shown below with the stream conditions. We will be processing toluene and hydrogen to produce at least 370 TPY of benzene. As shown in the flowsheet, there are two flash tanks, F101 to separate out the non-condensibles and F102 to further separate the benzene-toluene mixture to improve the benzene purity. Note that typically a distillation column is required to obtain high purity benzene but that is beyond the scope of this workshop. The non-condensibles separated out in F101 will be partially recycled back to M101 and the rest will be either purged or combusted for power generation.We will assume ideal gas for this flowsheet. The properties required for this module are available in the same directory:\n",
- "\n",
- "- hda_ideal_VLE.py\n",
- "- hda_reaction.py\n",
- "\n",
- "The state variables chosen for the property package are **flows of component by phase, temperature and pressure**. The components considered are: **toluene, hydrogen, benzene and methane**. Therefore, every stream has 8 flow variables, 1 temperature and 1 pressure variable. \n",
- "\n",
- "\n",
- "\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 package. 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",
- "- SolverFactory (to solve the problem)\n",
- "- TransformationFactory (to apply certain transformations)\n",
- "- Arc (to connect two unit models)\n",
- "- SequentialDecomposition (to initialize the flowsheet in a sequential mode)\n",
- "\n",
- "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\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",
- " SolverFactory,\n",
- " TransformationFactory,\n",
- " value,\n",
- ")\n",
- "from pyomo.network import Arc, SequentialDecomposition"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "From idaes, we will be needing the FlowsheetBlock and the following unit models:\n",
- "- Mixer\n",
- "- Heater\n",
- "- StoichiometricReactor\n",
- "-
**Flash**\n",
- "- Separator (splitter) \n",
- "- PressureChanger"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.core import FlowsheetBlock"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.models.unit_models import (\n",
- " PressureChanger,\n",
- " Mixer,\n",
- " Separator as Splitter,\n",
- " Heater,\n",
- " StoichiometricReactor,\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "Now, import the remaining unit models highlighted in blue above and run the cell using `Shift+Enter` after typing in the code. \n",
- "
\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: import flash model from idaes.models.unit_models\n",
- "from idaes.models.unit_models import Flash"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.models.unit_models.pressure_changer import ThermodynamicAssumption\n",
- "from idaes.core.util.model_statistics import degrees_of_freedom\n",
- "\n",
- "# Import idaes logger to set output levels\n",
- "import idaes.logger as idaeslog\n",
- "from idaes.core.solvers import get_solver\n",
- "from idaes.core.util.exceptions import InitializationError"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Importing required thermo and reaction package\n",
- "\n",
- "The final set of imports are to import the thermo and reaction package for the HDA process. We have created a custom thermo package that assumes Ideal Gas with support for VLE. \n",
- "\n",
- "The reaction package here is very simple as we will be using only a StochiometricReactor and the reaction package consists of the stochiometric coefficients for the reaction and the parameter for the heat of reaction. \n",
- "\n",
- "Let us import the following modules and they are in the same directory as this jupyter notebook:\n",
- "
\n",
- " - hda_ideal_VLE as thermo_props
\n",
- " - hda_reaction as reaction_props
\n",
- "
\n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes_examples.mod.hda import hda_ideal_VLE as thermo_props\n",
- "from idaes_examples.mod.hda import hda_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 as we did in module 1. "
- ]
- },
- {
- "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 Module 1, where we only had a thermo property package, for this flowsheet we will also need to add a reaction property package. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.thermo_params = thermo_props.HDAParameterBlock()\n",
- "m.fs.reaction_params = reaction_props.HDAReactionParameterBlock(\n",
- " property_package=m.fs.thermo_params\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 the Mixer (assigned a name M101) and a Heater (assigned a name H101). 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 (https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/unit_models/index.html). For example, the Mixer unit model here is given a `list` consisting of names to the three inlets. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.M101 = Mixer(\n",
- " property_package=m.fs.thermo_params,\n",
- " inlet_list=[\"toluene_feed\", \"hydrogen_feed\", \"vapor_recycle\"],\n",
- ")\n",
- "\n",
- "m.fs.H101 = Heater(\n",
- " property_package=m.fs.thermo_params,\n",
- " has_pressure_change=False,\n",
- " has_phase_equilibrium=True,\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "
Inline Exercise:\n",
- "Let us now add the StoichiometricReactor(assign the name R101) and pass the following arguments:\n",
- "
\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",
- "
\n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Add reactor with the specifications above\n",
- "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": [
- "Let us now add the Flash(assign the name F101) and pass the following arguments:\n",
- "
\n",
- " - \"property_package\": m.fs.thermo_params
\n",
- " - \"has_heat_transfer\": True
\n",
- " - \"has_pressure_change\": False
\n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.F101 = Flash(\n",
- " property_package=m.fs.thermo_params,\n",
- " has_heat_transfer=True,\n",
- " has_pressure_change=True,\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us now add the Splitter(S101), PressureChanger(C101) and the second Flash(F102). "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.S101 = Splitter(\n",
- " property_package=m.fs.thermo_params,\n",
- " ideal_separation=False,\n",
- " outlet_list=[\"purge\", \"recycle\"],\n",
- ")\n",
- "\n",
- "\n",
- "m.fs.C101 = PressureChanger(\n",
- " property_package=m.fs.thermo_params,\n",
- " compressor=True,\n",
- " thermodynamic_assumption=ThermodynamicAssumption.isothermal,\n",
- ")\n",
- "\n",
- "m.fs.F102 = Flash(\n",
- " property_package=m.fs.thermo_params,\n",
- " has_heat_transfer=True,\n",
- " has_pressure_change=True,\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(M101) to the inlet of the heater(H101). "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- " \n",
- "\n",
- "
\n",
- "Inline Exercise:\n",
- "Now, connect the H101 outlet to the R101 inlet using the cell above as a guide. \n",
- "
\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Connect the H101 outlet to R101 inlet\n",
- "m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We will now be connecting the rest of the flowsheet as shown below. Notice how the outlet names are different for the flash tanks F101 and F102 as they have a vapor and a liquid outlet. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.F101.inlet)\n",
- "m.fs.s06 = Arc(source=m.fs.F101.vap_outlet, destination=m.fs.S101.inlet)\n",
- "m.fs.s08 = Arc(source=m.fs.S101.recycle, destination=m.fs.C101.inlet)\n",
- "m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.vapor_recycle)\n",
- "m.fs.s10 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We have now connected the unit model block using the arcs. However, each of these arcs link to ports on the two unit models that are connected. In this case, the ports consist of the state variables that need to be linked between the unit models. Pyomo provides a convenient method to write these equality constraints for us between two ports and this is done as follows:"
- ]
- },
- {
- "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 purity and 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: https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html\n",
- "\n",
- "For this flowsheet, we are interested in computing the purity of the product Benzene stream (i.e. the mole fraction) and the operating cost which is a sum of the cooling and heating cost. "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us first add an Expression to compute the mole fraction of benzene in the `vap_outlet` of F102 which is our product stream. Please note that the var flow_mol_phase_comp has the index - [time, phase, component]. As this is a steady-state flowsheet, the time index by default is 0. The valid phases are [\"Liq\", \"Vap\"]. Similarly the valid component list is [\"benzene\", \"toluene\", \"hydrogen\", \"methane\"]."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.purity = Expression(\n",
- " expr=m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- " / (\n",
- " m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- " + m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
- " )\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now, let us add an expression to compute the cooling cost assuming a cost of 0.212E-4 $/kW. Note that cooling utility is required for the reactor (R101) and the first flash (F101). "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.cooling_cost = Expression(\n",
- " expr=0.212e-7 * (-m.fs.F101.heat_duty[0]) + 0.212e-7 * (-m.fs.R101.heat_duty[0])\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- "Now, let us add an expression to compute the heating cost assuming the utility cost as follows:\n",
- "
\n",
- " - 2.2E-4 dollars/kW for H101
\n",
- " - 1.9E-4 dollars/kW for F102
\n",
- "
\n",
- "Note that the heat duty is in units of watt (J/s). "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.heating_cost = Expression(\n",
- " expr=2.2e-7 * m.fs.H101.heat_duty[0] + 1.9e-7 * m.fs.F102.heat_duty[0]\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us now add an expression to compute the total operating cost per year which is basically the sum of the cooling and heating cost we defined above. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.operating_cost = Expression(\n",
- " expr=(3600 * 24 * 365 * (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. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(degrees_of_freedom(m))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We will now be fixing the toluene feed stream to the conditions shown in the flowsheet above. Please note that though this is a pure toluene feed, the remaining components are still assigned a very small non-zero value to help with convergence and initializing. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"toluene\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"hydrogen\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"methane\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"toluene\"].fix(0.30)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"hydrogen\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"methane\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.temperature.fix(303.2)\n",
- "m.fs.M101.toluene_feed.pressure.fix(350000)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- "Similarly, let us fix the hydrogen feed to the following conditions in the next cell:\n",
- "
\n",
- " - FH2 = 0.30 mol/s
\n",
- " - FCH4 = 0.02 mol/s
\n",
- " - Remaining components = 1e-5 mol/s
\n",
- " - T = 303.2 K
\n",
- " - P = 350000 Pa
\n",
- "
\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"toluene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"hydrogen\"].fix(0.30)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"methane\"].fix(0.02)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"toluene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"hydrogen\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"methane\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.temperature.fix(303.2)\n",
- "m.fs.M101.hydrogen_feed.pressure.fix(350000)"
- ]
- },
- {
- "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 set set the H101 outlet temperature to 600 K. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.H101.outlet.temperature.fix(600)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "For the StoichiometricReactor, we have to define the conversion in terms of toluene. This requires us to create a new variable for specifying the conversion and adding a Constraint that defines the conversion with respect to toluene. The second degree of freedom for the reactor is to define the heat duty. In this case, let us assume the reactor to be adiabatic i.e. Q = 0. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.R101.conversion = Var(initialize=0.75, bounds=(0, 1))\n",
- "\n",
- "m.fs.R101.conv_constraint = Constraint(\n",
- " expr=m.fs.R101.conversion * m.fs.R101.inlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
- " == (\n",
- " m.fs.R101.inlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
- " - m.fs.R101.outlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
- " )\n",
- ")\n",
- "\n",
- "m.fs.R101.conversion.fix(0.75)\n",
- "m.fs.R101.heat_duty.fix(0)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The Flash conditions for F101 can be set as follows. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.F101.vap_outlet.temperature.fix(325.0)\n",
- "m.fs.F101.deltaP.fix(0)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "
Inline Exercise:\n",
- "Set the conditions for Flash F102 to the following conditions:\n",
- "
\n",
- " - T = 375 K
\n",
- " - deltaP = -200000
\n",
- "
\n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "m.fs.F102.vap_outlet.temperature.fix(375)\n",
- "m.fs.F102.deltaP.fix(-200000)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us fix the purge split fraction to 20% and the outlet pressure of the compressor is set to 350000 Pa. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.S101.split_fraction[0, \"purge\"].fix(0.2)\n",
- "m.fs.C101.outlet.pressure.fix(350000)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "We have now defined all the feed conditions and the inputs required for the unit models. The system should now have 0 degrees of freedom i.e. should be a square problem. Please check that the degrees of freedom is 0. \n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "print(degrees_of_freedom(m))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Initialization\n",
- "\n",
- "\n",
- "This section will demonstrate how to use the built-in sequential decomposition tool to initialize our flowsheet.\n",
- "\n",
- " \n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us first create an object for the SequentialDecomposition and specify our options for this. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "seq = SequentialDecomposition()\n",
- "seq.options.select_tear_method = \"heuristic\"\n",
- "seq.options.tear_method = \"Wegstein\"\n",
- "seq.options.iterLim = 3\n",
- "\n",
- "# Using the SD tool\n",
- "G = seq.create_graph(m)\n",
- "heuristic_tear_set = seq.tear_set_arcs(G, method=\"heuristic\")\n",
- "order = seq.calculation_order(G)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Which is the tear stream? Display tear set and order"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "for o in heuristic_tear_set:\n",
- " print(o.name)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "What sequence did the SD tool determine to solve this flowsheet with the least number of tears? "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "scrolled": true
- },
- "outputs": [],
- "source": [
- "for o in order:\n",
- " print(o[0].name)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- " \n",
- "\n",
- " \n",
- "\n",
- "\n",
- "The SequentialDecomposition tool has determined that the tear stream is the mixer outlet. We will need to provide a reasonable guess for this."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "tear_guesses = {\n",
- " \"flow_mol_phase_comp\": {\n",
- " (0, \"Vap\", \"benzene\"): 1e-5,\n",
- " (0, \"Vap\", \"toluene\"): 1e-5,\n",
- " (0, \"Vap\", \"hydrogen\"): 0.30,\n",
- " (0, \"Vap\", \"methane\"): 0.02,\n",
- " (0, \"Liq\", \"benzene\"): 1e-5,\n",
- " (0, \"Liq\", \"toluene\"): 0.30,\n",
- " (0, \"Liq\", \"hydrogen\"): 1e-5,\n",
- " (0, \"Liq\", \"methane\"): 1e-5,\n",
- " },\n",
- " \"temperature\": {0: 303},\n",
- " \"pressure\": {0: 350000},\n",
- "}\n",
- "\n",
- "# Pass the tear_guess to the SD tool\n",
- "seq.set_guesses_for(m.fs.H101.inlet, tear_guesses)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Next, we need to tell the tool how to initialize a particular unit. We will be writing a python function which takes in a \"unit\" and calls the initialize method on that unit. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def function(unit):\n",
- " try:\n",
- " initializer = unit.default_initializer()\n",
- " initializer.initialize(unit, output_level=idaeslog.INFO)\n",
- " except InitializationError:\n",
- " solver = get_solver()\n",
- " solver.solve(unit)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We are now ready to initialize our flowsheet in a sequential mode. Note that we specifically set the iteration limit to be 5 as we are trying to use this tool only to get a good set of initial values such that IPOPT can then take over and solve this flowsheet for us. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "seq.run(m, function)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "We have now initialized the flowsheet. Let us run the flowsheet in a simulation mode to look at the results. To do this, complete the last line of code where we pass the model to the solver. You will need to type the following:\n",
- " \n",
- "results = solver.solve(m, tee=True)\n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Create the solver object\n",
- "from idaes.core.solvers import get_solver\n",
- "\n",
- "solver = get_solver()\n",
- "\n",
- "# 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(\"operating cost = $\", value(m.fs.operating_cost))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "For this operating cost, what is the amount of benzene we are able to produce and what purity we are able to achieve? "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.F102.report()\n",
- "\n",
- "print()\n",
- "print(\"benzene purity = \", value(m.fs.purity))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Next, let's look at how much benzene we are losing with the light gases out of F101. IDAES has tools for creating stream tables based on the `Arcs` and/or `Ports` in a flowsheet. Let us create and print a simple stream table showing the stream leaving the reactor and the vapor stream from F101.\n",
- "\n",
- "
\n",
- "Inline Exercise:\n",
- "How much benzene are we losing in the F101 vapor outlet stream?\n",
- "
\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.core.util.tables import (\n",
- " create_stream_table_dataframe,\n",
- " stream_table_dataframe_to_string,\n",
- ")\n",
- "\n",
- "st = create_stream_table_dataframe({\"Reactor\": m.fs.s05, \"Light Gases\": m.fs.s06})\n",
- "print(stream_table_dataframe_to_string(st))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "You can query additional variables here if you like. \n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Optimization\n",
- "\n",
- "\n",
- "We saw from the results above that the total operating cost for the base case was $419,122 per year. We are producing 0.142 mol/s of benzene at a purity of 82\\%. However, we are losing around 42\\% of benzene in F101 vapor outlet stream. \n",
- "\n",
- "Let us try to minimize this cost such that:\n",
- "- we are producing at least 0.15 mol/s of benzene in F102 vapor outlet i.e. our product stream\n",
- "- purity of benzene i.e. the mole fraction of benzene in F102 vapor outlet is at least 80%\n",
- "- restricting the benzene loss in F101 vapor outlet to less than 20%\n",
- "\n",
- "For this problem, our decision variables are as follows:\n",
- "- H101 outlet temperature\n",
- "- R101 cooling duty provided\n",
- "- F101 outlet temperature\n",
- "- F102 outlet temperature\n",
- "- F102 deltaP in the flash tank\n"
- ]
- },
- {
- "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 unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.H101.outlet.temperature.unfix()\n",
- "m.fs.R101.heat_duty.unfix()\n",
- "m.fs.F101.vap_outlet.temperature.unfix()\n",
- "m.fs.F102.vap_outlet.temperature.unfix()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "Let us now unfix the remaining variable which is F102 pressure drop (F102.deltaP) \n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Unfix deltaP for F102\n",
- "m.fs.F102.deltaP.unfix()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Next, we need to set bounds on these decision variables to values shown below:\n",
- "\n",
- " - H101 outlet temperature [500, 600] K\n",
- " - R101 outlet temperature [600, 800] K\n",
- " - F101 outlet temperature [298, 450] K\n",
- " - F102 outlet temperature [298, 450] K\n",
- " - F102 outlet pressure [105000, 110000] Pa\n",
- "\n",
- "Let us first set the variable bound for the H101 outlet temperature as shown below:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.H101.outlet.temperature[0].setlb(500)\n",
- "m.fs.H101.outlet.temperature[0].setub(600)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "Now, set the variable bound for the R101 outlet temperature.\n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Set the bounds for reactor outlet temperature\n",
- "m.fs.R101.outlet.temperature[0].setlb(600)\n",
- "m.fs.R101.outlet.temperature[0].setub(800)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us fix the bounds for the rest of the decision variables. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.F101.vap_outlet.temperature[0].setlb(298.0)\n",
- "m.fs.F101.vap_outlet.temperature[0].setub(450.0)\n",
- "m.fs.F102.vap_outlet.temperature[0].setlb(298.0)\n",
- "m.fs.F102.vap_outlet.temperature[0].setub(450.0)\n",
- "m.fs.F102.vap_outlet.pressure[0].setlb(105000)\n",
- "m.fs.F102.vap_outlet.pressure[0].setub(110000)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now, the only things left to define are our constraints on overhead loss in F101, product flow rate and purity in F102. Let us first look at defining a constraint for the overhead loss in F101 where we are restricting the benzene leaving the vapor stream to less than 20 \\% of the benzene available in the reactor outlet. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.overhead_loss = Constraint(\n",
- " expr=m.fs.F101.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- " <= 0.20 * m.fs.R101.outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "Now, add the constraint such that we are producing at least 0.15 mol/s of benzene in the product stream which is the vapor outlet of F102. Let us name this constraint as m.fs.product_flow. \n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Add minimum product flow constraint\n",
- "m.fs.product_flow = Constraint(\n",
- " expr=m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"] >= 0.15\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us add the final constraint on product purity or the mole fraction of benzene in the product stream such that it is at least greater than 80%. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.product_purity = Constraint(expr=m.fs.purity >= 0.80)"
- ]
- },
- {
- "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": "markdown",
- "metadata": {},
- "source": [
- "## Optimization Results\n",
- "\n",
- "Display the results and product specifications"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(\"operating cost = $\", value(m.fs.operating_cost))\n",
- "\n",
- "print()\n",
- "print(\"Product flow rate and purity in F102\")\n",
- "\n",
- "m.fs.F102.report()\n",
- "\n",
- "print()\n",
- "print(\"benzene purity = \", value(m.fs.purity))\n",
- "\n",
- "print()\n",
- "print(\"Overhead loss in F101\")\n",
- "m.fs.F101.report()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Display optimal values for the decision variables"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(\"Optimal Values\")\n",
- "print()\n",
- "\n",
- "print(\"H101 outlet temperature = \", value(m.fs.H101.outlet.temperature[0]), \"K\")\n",
- "\n",
- "print()\n",
- "print(\"R101 outlet temperature = \", value(m.fs.R101.outlet.temperature[0]), \"K\")\n",
- "\n",
- "print()\n",
- "print(\"F101 outlet temperature = \", value(m.fs.F101.vap_outlet.temperature[0]), \"K\")\n",
- "\n",
- "print()\n",
- "print(\"F102 outlet temperature = \", value(m.fs.F102.vap_outlet.temperature[0]), \"K\")\n",
- "print(\"F102 outlet pressure = \", value(m.fs.F102.vap_outlet.pressure[0]), \"Pa\")"
- ]
- },
- {
- "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.8.12"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 3
+ "cells": [
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "header",
+ "hide-cell"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:27.028013200Z",
+ "start_time": "2026-01-13T00:12:27.015275700Z"
+ }
+ },
+ "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",
+ "###############################################################################"
+ ],
+ "outputs": [],
+ "execution_count": 1
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "# HDA Flowsheet Simulation and Optimization\n",
+ "\n",
+ "Author: Jaffer Ghouse
\n",
+ "Maintainer: Tanner Polley
\n",
+ "Updated: 2026-1-12\n",
+ "\n",
+ "## Learning outcomes\n",
+ "\n",
+ "\n",
+ "- Construct a steady-state flowsheet using the IDAES unit model library\n",
+ "- Connecting unit models in a flowsheet using Arcs\n",
+ "- Using the SequentialDecomposition tool to initialize a flowsheet with recycle\n",
+ "- Formulate and solve an optimization problem\n",
+ " - Defining an objective function\n",
+ " - Setting variable bounds\n",
+ " - Adding additional constraints\n",
+ "\n",
+ "\n",
+ "The general workflow of setting up an IDAES flowsheet is the following:\n",
+ "\n",
+ " 1 Importing Modules
\n",
+ " 2 Building a Model
\n",
+ " 3 Scaling the Model
\n",
+ " 4 Specifying the Model
\n",
+ " 5 Initializing the Model
\n",
+ " 6 Solving the Model
\n",
+ " 7 Analyzing and Visualizing the Results
\n",
+ " 8 Optimizing the Model
\n",
+ "\n",
+ "We will complete each of these steps as well as demonstrate analyses on this model through some examples and exercises.\n",
+ "\n",
+ "\n",
+ "## Problem Statement\n",
+ "\n",
+ "Hydrodealkylation is a chemical reaction that often involves reacting\n",
+ "an aromatic hydrocarbon in the presence of hydrogen gas to form a\n",
+ "simpler aromatic hydrocarbon devoid of functional groups. In this\n",
+ "example, toluene will be reacted with hydrogen gas at high temperatures\n",
+ " to form benzene via the following reaction:\n",
+ "\n",
+ "**C
6H
5CH
3 + H
2 \u2192 C
6H
6 + CH
4**\n",
+ "\n",
+ "\n",
+ "This reaction is often accompanied by an equilibrium side reaction\n",
+ "which forms diphenyl, which we will neglect for this example.\n",
+ "\n",
+ "This example is based on the 1967 AIChE Student Contest problem as\n",
+ "present by Douglas, J.M., Chemical Design of Chemical Processes, 1988,\n",
+ "McGraw-Hill.\n",
+ "\n",
+ "The flowsheet that we will be using for this module is shown below with the stream conditions. We will be processing toluene and hydrogen to produce at least 370 TPY of benzene. As shown in the flowsheet, there are two flash tanks, F101 to separate out the non-condensibles and F102 to further separate the benzene-toluene mixture to improve the benzene purity. Note that typically a distillation column is required to obtain high purity benzene but that is beyond the scope of this workshop. The non-condensibles separated out in F101 will be partially recycled back to M101 and the rest will be either purged or combusted for power generation.We will assume ideal gas for this flowsheet. The properties required for this module are available in the same directory:\n",
+ "\n",
+ "- hda_ideal_VLE.py\n",
+ "- hda_reaction.py\n",
+ "\n",
+ "The state variables chosen for the property package are **flows of component by phase, temperature and pressure**. The components considered are: **toluene, hydrogen, benzene and methane**. Therefore, every stream has 8 flow variables, 1 temperature and 1 pressure variable. \n",
+ "\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 1 Importing Modules\n",
+ "### 1.1 Importing required Pyomo and IDAES components\n",
+ "\n",
+ "\n",
+ "To construct a flowsheet, we will need several components from the Pyomo and IDAES package. 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",
+ "- SolverFactory (to solve the problem)\n",
+ "- TransformationFactory (to apply certain transformations)\n",
+ "- Arc (to connect two unit models)\n",
+ "- SequentialDecomposition (to initialize the flowsheet in a sequential mode)\n",
+ "\n",
+ "For further details on these components, please refer to the Pyomo documentation: https://Pyomo.readthedocs.io/en/stable/\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:27.614407500Z",
+ "start_time": "2026-01-13T00:12:27.029098600Z"
+ }
+ },
+ "source": [
+ "from pyomo.environ import (\n",
+ " Constraint,\n",
+ " Var,\n",
+ " ConcreteModel,\n",
+ " Expression,\n",
+ " Objective,\n",
+ " TransformationFactory,\n",
+ " value,\n",
+ ")\n",
+ "from pyomo.network import Arc"
+ ],
+ "outputs": [],
+ "execution_count": 2
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "From IDAES, we will be needing the FlowsheetBlock and the following unit models:\n",
+ "- Feed\n",
+ "- Mixer\n",
+ "- Heater\n",
+ "- StoichiometricReactor\n",
+ "-
**Flash**\n",
+ "- Separator (splitter) \n",
+ "- PressureChanger\n",
+ "- Product"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.427113300Z",
+ "start_time": "2026-01-13T00:12:27.699286500Z"
+ }
+ },
+ "source": "from idaes.core import FlowsheetBlock",
+ "outputs": [],
+ "execution_count": 3
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.495404700Z",
+ "start_time": "2026-01-13T00:12:29.432738900Z"
+ }
+ },
+ "source": [
+ "from idaes.models.unit_models import (\n",
+ " PressureChanger,\n",
+ " Mixer,\n",
+ " Separator as Splitter,\n",
+ " Heater,\n",
+ " StoichiometricReactor,\n",
+ " Feed,\n",
+ " Product,\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 4
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "
\n",
+ "Inline Exercise:\n",
+ "Now, import the remaining unit models highlighted in blue above and run the cell using `Shift+Enter` after typing in the code. \n",
+ "
\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "solution"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.528819Z",
+ "start_time": "2026-01-13T00:12:29.505935Z"
+ }
+ },
+ "source": [
+ "# Todo: import flash model from idaes.models.unit_models\n",
+ "from idaes.models.unit_models import Flash"
+ ],
+ "outputs": [],
+ "execution_count": 6
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.539485800Z",
+ "start_time": "2026-01-13T00:12:29.531348400Z"
+ }
+ },
+ "source": [
+ "from idaes.models.unit_models.pressure_changer import ThermodynamicAssumption\n",
+ "from idaes.core.util.model_statistics import degrees_of_freedom\n",
+ "\n",
+ "# Import idaes logger to set output levels\n",
+ "from idaes.core.solvers import get_solver"
+ ],
+ "outputs": [],
+ "execution_count": 7
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 1.2 Importing required thermo and reaction package\n",
+ "\n",
+ "The final set of imports are to import the thermo and reaction package for the HDA process. We have created a custom thermo package that assumes Ideal Gas with support for VLE. \n",
+ "\n",
+ "The reaction package here is very simple as we will be using only a StochiometricReactor and the reaction package consists of the stochiometric coefficients for the reaction and the parameter for the heat of reaction. \n",
+ "\n",
+ "Let us import the following modules and they are in the same directory as this jupyter notebook:\n",
+ "
\n",
+ " - hda_ideal_VLE as thermo_props
\n",
+ " - hda_reaction as reaction_props
\n",
+ "
\n",
+ "
"
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.627980300Z",
+ "start_time": "2026-01-13T00:12:29.540486900Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "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_examples.mod.hda.hda_ideal_VLE_modular import thermo_config\n",
+ "from idaes_examples.mod.hda.hda_reaction_modular import reaction_config"
+ ],
+ "outputs": [],
+ "execution_count": 8
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 2 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",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.638672500Z",
+ "start_time": "2026-01-13T00:12:29.630018Z"
+ }
+ },
+ "source": [
+ "m = ConcreteModel()\n",
+ "m.fs = FlowsheetBlock(dynamic=False)"
+ ],
+ "outputs": [],
+ "execution_count": 9
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We now need to add the property packages to the flowsheet. Unlike Module 1, where we only had a thermo property package, for this flowsheet we will also need to add a reaction property package. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.669834600Z",
+ "start_time": "2026-01-13T00:12:29.638672500Z"
+ }
+ },
+ "source": [
+ "m.fs.thermo_params = GenericParameterBlock(**thermo_config)\n",
+ "m.fs.reaction_params = GenericReactionParameterBlock(\n",
+ " property_package=m.fs.thermo_params, **reaction_config\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 10
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 2.1 Adding Unit Models\n",
+ "\n",
+ "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding the Feed (assigned a name `I101` for Inlet), `Mixer` (assigned a name `M101`) and a `Heater` (assigned a name `H101`). 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 (https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/unit_models/index.html). For example, the `Mixer` unit model here must be specified the number of inlets that it will take in and the `Heater` can have specific settings enabled such as `has_pressure_change` or `has_phase_equilibrium`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.718708300Z",
+ "start_time": "2026-01-13T00:12:29.672813900Z"
+ }
+ },
+ "source": [
+ "m.fs.I101 = Feed(property_package=m.fs.thermo_params)\n",
+ "m.fs.I102 = Feed(property_package=m.fs.thermo_params)\n",
+ "\n",
+ "m.fs.M101 = Mixer(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " num_inlets=3,\n",
+ ")\n",
+ "\n",
+ "m.fs.H101 = Heater(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " has_pressure_change=False,\n",
+ " has_phase_equilibrium=True,\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 11
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "solution"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.761429600Z",
+ "start_time": "2026-01-13T00:12:29.739837200Z"
+ }
+ },
+ "source": [
+ "# Todo: Add reactor with the specifications above\n",
+ "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",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 13
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us now add the Flash(assign the name F101) and pass the following arguments:\n",
+ "
\n",
+ " - \"property_package\": m.fs.thermo_params
\n",
+ " - \"has_heat_transfer\": True
\n",
+ " - \"has_pressure_change\": False
\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.785139300Z",
+ "start_time": "2026-01-13T00:12:29.762955300Z"
+ }
+ },
+ "source": [
+ "m.fs.F101 = Flash(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " has_heat_transfer=True,\n",
+ " has_pressure_change=True,\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 14
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us now add the Splitter(S101) with specific names for its output (purge and recycle), PressureChanger(C101) and the second Flash(F102)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.827217600Z",
+ "start_time": "2026-01-13T00:12:29.787651900Z"
+ }
+ },
+ "source": [
+ "m.fs.S101 = Splitter(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " ideal_separation=False,\n",
+ " outlet_list=[\"purge\", \"recycle\"],\n",
+ ")\n",
+ "\n",
+ "\n",
+ "m.fs.C101 = PressureChanger(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " compressor=True,\n",
+ " thermodynamic_assumption=ThermodynamicAssumption.isothermal,\n",
+ ")\n",
+ "\n",
+ "m.fs.F102 = Flash(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " has_heat_transfer=True,\n",
+ " has_pressure_change=True,\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 15
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Last, we will add the three Product blocks (P101, P102, P103). We use `Feed` blocks and `Product` blocks for convenience with reporting stream summaries and consistency"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.844845200Z",
+ "start_time": "2026-01-13T00:12:29.829763300Z"
+ }
+ },
+ "source": [
+ "m.fs.P101 = Product(property_package=m.fs.thermo_params)\n",
+ "m.fs.P102 = Product(property_package=m.fs.thermo_params)\n",
+ "m.fs.P103 = Product(property_package=m.fs.thermo_params)"
+ ],
+ "outputs": [],
+ "execution_count": 16
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 2.2 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 inlets (I101, I102) to the inlet of the mixer (M101) and outlet of the mixer to the inlet of the heater(H101)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.855598100Z",
+ "start_time": "2026-01-13T00:12:29.847349400Z"
+ }
+ },
+ "source": [
+ "m.fs.s01 = Arc(source=m.fs.I101.outlet, destination=m.fs.M101.inlet_1)\n",
+ "m.fs.s02 = Arc(source=m.fs.I102.outlet, destination=m.fs.M101.inlet_2)\n",
+ "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)"
+ ],
+ "outputs": [],
+ "execution_count": 17
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "solution"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.873064300Z",
+ "start_time": "2026-01-13T00:12:29.865134200Z"
+ }
+ },
+ "source": [
+ "# Todo: Connect the H101 outlet to R101 inlet\n",
+ "m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet)"
+ ],
+ "outputs": [],
+ "execution_count": 19
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We will now be connecting the rest of the flowsheet as shown below. Notice how the outlet names are different for the flash tanks F101 and F102 as they have a vapor and a liquid outlet. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.882527100Z",
+ "start_time": "2026-01-13T00:12:29.874065200Z"
+ }
+ },
+ "source": [
+ "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.F101.inlet)\n",
+ "m.fs.s06 = Arc(source=m.fs.F101.vap_outlet, destination=m.fs.S101.inlet)\n",
+ "m.fs.s07 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet)\n",
+ "m.fs.s08 = Arc(source=m.fs.S101.recycle, destination=m.fs.C101.inlet)\n",
+ "m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.inlet_3)"
+ ],
+ "outputs": [],
+ "execution_count": 20
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Last we will connect the outlet streams to the inlets of the Product blocks (P101, P102, P103)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.891441800Z",
+ "start_time": "2026-01-13T00:12:29.882527100Z"
+ }
+ },
+ "source": [
+ "m.fs.s10 = Arc(source=m.fs.F102.vap_outlet, destination=m.fs.P101.inlet)\n",
+ "m.fs.s11 = Arc(source=m.fs.F102.liq_outlet, destination=m.fs.P102.inlet)\n",
+ "m.fs.s12 = Arc(source=m.fs.S101.purge, destination=m.fs.P103.inlet)"
+ ],
+ "outputs": [],
+ "execution_count": 21
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We have now connected the unit model block using the arcs. However, each of these arcs link to ports on the two unit models that are connected. In this case, the ports consist of the state variables that need to be linked between the unit models. Pyomo provides a convenient method to write these equality constraints for us between two ports and this is done as follows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.916831200Z",
+ "start_time": "2026-01-13T00:12:29.891441800Z"
+ }
+ },
+ "source": [
+ "TransformationFactory(\"network.expand_arcs\").apply_to(m)"
+ ],
+ "outputs": [],
+ "execution_count": 22
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 2.3 Adding expressions to compute purity and 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: https://pyomo.readthedocs.io/en/stable/explanation/modeling/network.html.\n",
+ "\n",
+ "For this flowsheet, we are interested in computing the purity of the product Benzene stream (i.e. the mole fraction) and the operating cost which is a sum of the cooling and heating cost. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us first add an Expression to compute the mole fraction of benzene in the `vap_outlet` of F102 which is our product stream. Please note that the var flow_mol_phase_comp has the index - [time, phase, component]. As this is a steady-state flowsheet, the time index by default is 0. The valid phases are [\"Liq\", \"Vap\"]. Similarly the valid component list is [\"benzene\", \"toluene\", \"hydrogen\", \"methane\"]."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.926950800Z",
+ "start_time": "2026-01-13T00:12:29.918895100Z"
+ }
+ },
+ "source": [
+ "m.fs.purity = Expression(\n",
+ " expr=m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"benzene\"\n",
+ " ]\n",
+ " / (\n",
+ " m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\"Vap\", \"benzene\"]\n",
+ " + m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"toluene\"\n",
+ " ]\n",
+ " )\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 23
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, let us add an expression to compute the cooling cost assuming a cost of 0.212E-4 $/kW. Note that cooling utility is required for the reactor (R101) and the first flash (F101). "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.937452200Z",
+ "start_time": "2026-01-13T00:12:29.928143Z"
+ }
+ },
+ "source": [
+ "m.fs.cooling_cost = Expression(\n",
+ " expr=0.212e-7 * (-m.fs.F101.heat_duty[0]) + 0.212e-7 * (-m.fs.R101.heat_duty[0])\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 24
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "Now, let us add an expression to compute the heating cost assuming the utility cost as follows:\n",
+ "
\n",
+ " - 2.2E-4 dollars/kW for H101
\n",
+ " - 1.9E-4 dollars/kW for F102
\n",
+ "
\n",
+ "Note that the heat duty is in units of watt (J/s). "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.945656300Z",
+ "start_time": "2026-01-13T00:12:29.938460700Z"
+ }
+ },
+ "source": [
+ "m.fs.heating_cost = Expression(\n",
+ " expr=2.2e-7 * m.fs.H101.heat_duty[0] + 1.9e-7 * m.fs.F102.heat_duty[0]\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 25
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us now add an expression to compute the total operating cost per year which is basically the sum of the cooling and heating cost we defined above. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.954918200Z",
+ "start_time": "2026-01-13T00:12:29.947160400Z"
+ }
+ },
+ "source": [
+ "m.fs.operating_cost = Expression(\n",
+ " expr=(3600 * 24 * 365 * (m.fs.heating_cost + m.fs.cooling_cost))\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 26
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 4 Specifying the Model\n",
+ "### 4.1 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. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.987120200Z",
+ "start_time": "2026-01-13T00:12:29.954918200Z"
+ }
+ },
+ "source": [
+ "print(degrees_of_freedom(m))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "29\n"
+ ]
+ }
+ ],
+ "execution_count": 27
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We will now be fixing the toluene feed (`I101`) stream to the conditions shown in the flowsheet above. Please note\n",
+ "that though this is a pure toluene feed, the remaining components are still assigned a very small non-zero value to\n",
+ "help with convergence and initializing. We will be importing a function that will specify the inlet conditions for\n",
+ "this example."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.037749600Z",
+ "start_time": "2026-01-13T00:12:30.025896900Z"
+ }
+ },
+ "source": [
+ "from idaes_examples.mod.hda.hda_flowsheet_extras import fix_inlet_states\n",
+ "\n",
+ "tear_guesses = fix_inlet_states(m)"
+ ],
+ "outputs": [],
+ "execution_count": 29
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 4.2 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 set set the H101 outlet temperature to 600 K. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.047997400Z",
+ "start_time": "2026-01-13T00:12:30.039779600Z"
+ }
+ },
+ "source": [
+ "m.fs.H101.outlet.temperature.fix(600)"
+ ],
+ "outputs": [],
+ "execution_count": 30
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For the StoichiometricReactor, we have to define the conversion in terms of toluene. This requires us to create a new variable for specifying the conversion and adding a Constraint that defines the conversion with respect to toluene. The second degree of freedom for the reactor is to define the heat duty. In this case, let us assume the reactor to be adiabatic i.e. Q = 0. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.057083400Z",
+ "start_time": "2026-01-13T00:12:30.047997400Z"
+ }
+ },
+ "source": [
+ "m.fs.R101.conversion = Var(initialize=0.75, bounds=(0, 1))\n",
+ "\n",
+ "m.fs.R101.conv_constraint = Constraint(\n",
+ " expr=m.fs.R101.conversion\n",
+ " * (m.fs.R101.control_volume.properties_in[0].flow_mol_phase_comp[\"Vap\", \"toluene\"])\n",
+ " == (\n",
+ " m.fs.R101.control_volume.properties_in[0].flow_mol_phase_comp[\"Vap\", \"toluene\"]\n",
+ " - m.fs.R101.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"toluene\"\n",
+ " ]\n",
+ " )\n",
+ ")\n",
+ "\n",
+ "m.fs.R101.conversion.fix(0.75)\n",
+ "m.fs.R101.heat_duty.fix(0)"
+ ],
+ "outputs": [],
+ "execution_count": 31
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The Flash conditions for F101 can be set as follows. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.064658700Z",
+ "start_time": "2026-01-13T00:12:30.057083400Z"
+ }
+ },
+ "source": [
+ "m.fs.F101.vap_outlet.temperature.fix(325.0)\n",
+ "m.fs.F101.deltaP.fix(0)"
+ ],
+ "outputs": [],
+ "execution_count": 32
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "solution"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.079966500Z",
+ "start_time": "2026-01-13T00:12:30.072457700Z"
+ }
+ },
+ "source": [
+ "m.fs.F102.vap_outlet.temperature.fix(375)\n",
+ "m.fs.F102.deltaP.fix(-200000)"
+ ],
+ "outputs": [],
+ "execution_count": 34
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us fix the purge split fraction to 20% and the outlet pressure of the compressor is set to 350000 Pa. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.088622800Z",
+ "start_time": "2026-01-13T00:12:30.079966500Z"
+ }
+ },
+ "source": [
+ "m.fs.S101.split_fraction[0, \"purge\"].fix(0.2)\n",
+ "m.fs.C101.outlet.pressure.fix(350000)"
+ ],
+ "outputs": [],
+ "execution_count": 35
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "solution"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.125313600Z",
+ "start_time": "2026-01-13T00:12:30.097272200Z"
+ }
+ },
+ "source": [
+ "print(degrees_of_freedom(m))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "0\n"
+ ]
+ }
+ ],
+ "execution_count": 37
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 5 Initializing the Model\n",
+ "\n",
+ "\n",
+ "\n",
+ "When a flowsheet contains a recycle loop, the outlet of a downstream unit becomes the inlet of an upstream unit, creating a cyclic dependency that prevents straightforward calculation of all stream conditions. The tear\u2010stream method is necessary because it \u201cbreaks\u201d this loop: you select one recycle stream as the tear, assign it an initial guess, and then solve the rest of the flowsheet as if it were acyclic. Once the downstream units compute their outputs, you compare the calculated value of the torn stream to your initial guess and iteratively adjust until they coincide. Without tearing, the solver cannot establish a proper topological sequence or drive the recycle to convergence, making initialization\u2014and ultimately steady\u2010state convergence\u2014impossible.\n",
+ "\n",
+ "It is important to determine the tear stream for a flowsheet which will be demonstrated below.\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "Currently, there are two methods of initializing a full flowsheet: using the sequential decomposition tool, or\n",
+ "manually propagating through the flowsheet. The tear stream in this example will be the stream from the mixer to the heater since that is where the\n",
+ "recycle stream first enters back into the main process.\n",
+ "\n"
+ ]
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "First, we will highlight some helpful functions that are used in the initialization process."
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.174558500Z",
+ "start_time": "2026-01-13T00:12:30.166118800Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "def initialize_unit(unit):\n",
+ " from idaes.core.util.exceptions import InitializationError\n",
+ " import idaes.logger as idaeslog\n",
+ "\n",
+ " optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 1000,\n",
+ " \"tol\": 1e-8,\n",
+ " }\n",
+ "\n",
+ " try:\n",
+ " initializer = unit.default_initializer(solver_options=optarg)\n",
+ " initializer.initialize(unit, output_level=idaeslog.INFO_LOW)\n",
+ " except InitializationError:\n",
+ " solver = get_solver(solver_options=optarg)\n",
+ " solver.solve(unit)"
+ ],
+ "outputs": [],
+ "execution_count": 39
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This first function will take any unit model and can either initialize the model with its respective default\n",
+ "initializer, or use a generic solver and the solve the current state of the unit model. Often times a direct\n",
+ "initialization method will fail while a solving method will converge so having the option for both is helpful.\n",
+ "\n",
+ "\n",
+ "### 5.1 Sequential Decomposition\n",
+ "\n",
+ "This section will demonstrate how to use the built-in sequential decomposition tool to initialize our flowsheet. Sequential Decomposition is a tool from Pyomo where the documentation can be found here https://Pyomo.readthedocs.io/en/stable/explanation/modeling/network.html#sequential-decomposition"
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.183526400Z",
+ "start_time": "2026-01-13T00:12:30.175559400Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "def automatic_propagation(m, tear_guesses):\n",
+ "\n",
+ " from pyomo.network import SequentialDecomposition\n",
+ "\n",
+ " seq = SequentialDecomposition()\n",
+ " seq.options.select_tear_method = \"heuristic\"\n",
+ " seq.options.tear_method = \"Wegstein\"\n",
+ " seq.options.iterLim = 5\n",
+ "\n",
+ " # Using the SD tool\n",
+ " G = seq.create_graph(m)\n",
+ " heuristic_tear_set = seq.tear_set_arcs(G, method=\"heuristic\")\n",
+ " order = seq.calculation_order(G)\n",
+ "\n",
+ " # Pass the tear_guess to the SD tool\n",
+ " seq.set_guesses_for(heuristic_tear_set[0].destination, tear_guesses)\n",
+ "\n",
+ " print(f\"Tear Stream starts at: {heuristic_tear_set[0].destination.name}\")\n",
+ "\n",
+ " for o in order:\n",
+ " print(o[0].name)\n",
+ "\n",
+ " seq.run(m, initialize_unit)"
+ ],
+ "outputs": [],
+ "execution_count": 40
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We are now ready to initialize our flowsheet in a sequential mode. Note that we specifically set the iteration limit\n",
+ "to be 5 as we are trying to use this tool only to get a good set of initial values such that IPOPT can then take over\n",
+ " and solve this flowsheet for us. Uncomment this function call to run the automatic propagation method"
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.191140600Z",
+ "start_time": "2026-01-13T00:12:30.184028800Z"
+ }
+ },
+ "cell_type": "code",
+ "source": "# automatic_propagation(m, tear_guesses)",
+ "outputs": [],
+ "execution_count": 41
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 5.2 Manual Propagation Method\n",
+ "\n",
+ "This method uses a more direct approach to initialize the flowsheet, using the updated initializer method and propagating manually through the flowsheet and solving for the tear stream directly.\n",
+ "Lets define the function that will help us manually propagate and step through the flowsheet"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.202920300Z",
+ "start_time": "2026-01-13T00:12:30.191140600Z"
+ }
+ },
+ "source": [
+ "def manual_propagation(m, tear_guesses):\n",
+ " from idaes.core.util.initialization import propagate_state\n",
+ "\n",
+ " print(f\"The DOF is {degrees_of_freedom(m)} initially\")\n",
+ " m.fs.s03_expanded.deactivate()\n",
+ " print(f\"The DOF is {degrees_of_freedom(m)} after deactivating the tear stream\")\n",
+ "\n",
+ " for k, v in tear_guesses.items():\n",
+ " for k1, v1 in v.items():\n",
+ " getattr(m.fs.s03.destination, k)[k1].fix(v1)\n",
+ "\n",
+ " print(f\"The DOF is {degrees_of_freedom(m)} after setting the tear stream\")\n",
+ "\n",
+ " optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 300,\n",
+ " # \"tol\": 1e-10,\n",
+ " }\n",
+ "\n",
+ " solver = get_solver(solver_options=optarg)\n",
+ "\n",
+ " initialize_unit(m.fs.H101) # Initialize Heater\n",
+ " propagate_state(m.fs.s04) # Establish connection between Heater and Reactor\n",
+ " initialize_unit(m.fs.R101) # Initialize Reactor\n",
+ " propagate_state(\n",
+ " m.fs.s05\n",
+ " ) # Establish connection between Reactor and First Flash Unit\n",
+ " initialize_unit(m.fs.F101) # Initialize First Flash Unit\n",
+ " propagate_state(\n",
+ " m.fs.s06\n",
+ " ) # Establish connection between First Flash Unit and Splitter\n",
+ " propagate_state(\n",
+ " m.fs.s07\n",
+ " ) # Establish connection between First Flash Unit and Second Flash Unit\n",
+ " initialize_unit(m.fs.S101) # Initialize Splitter\n",
+ " propagate_state(m.fs.s08) # Establish connection between Splitter and Compressor\n",
+ " initialize_unit(m.fs.C101) # Initialize Compressor\n",
+ " propagate_state(m.fs.s09) # Establish connection between Compressor and Mixer\n",
+ " initialize_unit(m.fs.I101) # Initialize Toluene Inlet\n",
+ " propagate_state(m.fs.s01) # Establish connection between Toluene Inlet and Mixer\n",
+ " initialize_unit(m.fs.I102) # Initialize Hydrogen Inlet\n",
+ " propagate_state(m.fs.s02) # Establish connection between Hydrogen Inlet and Mixer\n",
+ " initialize_unit(m.fs.M101) # Initialize Mixer\n",
+ " propagate_state(m.fs.s03) # Establish connection between Mixer and Heater\n",
+ " solver.solve(m.fs.F102)\n",
+ " propagate_state(\n",
+ " m.fs.s10\n",
+ " ) # Establish connection between Second Flash Unit and Benzene Product\n",
+ " propagate_state(\n",
+ " m.fs.s11\n",
+ " ) # Establish connection between Second Flash Unit and Toluene Product\n",
+ " propagate_state(m.fs.s12) # Establish connection between Splitter and Purge Product\n",
+ "\n",
+ " optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 300,\n",
+ " \"tol\": 1e-8,\n",
+ " }\n",
+ " solver = get_solver(\"ipopt_v2\", options=optarg)\n",
+ " solver.solve(m, tee=False)\n",
+ "\n",
+ " for k, v in tear_guesses.items():\n",
+ " for k1, v1 in v.items():\n",
+ " getattr(m.fs.H101.inlet, k)[k1].unfix()\n",
+ "\n",
+ " m.fs.s03_expanded.activate()\n",
+ " print(\n",
+ " f\"The DOF is {degrees_of_freedom(m)} after unfixing the values and reactivating the tear stream\"\n",
+ " )"
+ ],
+ "outputs": [],
+ "execution_count": 42
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "It will first show that the degrees of freedom is correctly at 0 before any streams are deactivated. Once the tear\n",
+ "stream is deactivated though, the degrees of freedom will be 10. That means 10 variables will have to be defined with\n",
+ " the tear guesses `tear_guesses`. Then each unit model can be initialized with our same helper function and then can\n",
+ " propagate the corresponding connection to the following unit models. At the end, the whole flowsheet is solved,\n",
+ " giving a much better chance for the recycle stream to be used correctly the flowsheet to converge."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.448895Z",
+ "start_time": "2026-01-13T00:12:30.204922600Z"
+ }
+ },
+ "source": "manual_propagation(m, tear_guesses)",
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The DOF is 0 initially\n",
+ "The DOF is 10 after deactivating the tear stream\n",
+ "The DOF is 0 after setting the tear stream\n",
+ "The DOF is 0 after unfixing the values and reactivating the tear stream\n"
+ ]
+ }
+ ],
+ "execution_count": 43
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 6 Solving the Model"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "We have now initialized the flowsheet. Lets set up some solving options before simulating the flowsheet. We want to specify the scaling method, number of iterations, and tolerance. More specific or advanced options can be found at the documentation for IPOPT https://coin-or.github.io/Ipopt/OPTIONS.html"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.467353Z",
+ "start_time": "2026-01-13T00:12:32.458309200Z"
+ }
+ },
+ "source": [
+ "optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 1000,\n",
+ " \"tol\": 1e-8,\n",
+ "}"
+ ],
+ "outputs": [],
+ "execution_count": 44
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "solution"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.967264500Z",
+ "start_time": "2026-01-13T00:12:32.476788800Z"
+ }
+ },
+ "source": [
+ "# Create the solver object\n",
+ "solver = get_solver(\"ipopt_v2\", options=optarg)\n",
+ "\n",
+ "# Solve the model\n",
+ "results = solver.solve(m, tee=False)"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Ipopt 3.13.2: linear_solver=\"ma57\"\n",
+ "max_iter=1000\n",
+ "nlp_scaling_method=\"user-scaling\"\n",
+ "tol=1e-08\n",
+ "option_file_name=\"C:\\Users\\Tanner\\AppData\\Local\\Temp\\tmpjzf4e172\\unknown.31496.43076.opt\"\n",
+ "\n",
+ "Using option file \"C:\\Users\\Tanner\\AppData\\Local\\Temp\\tmpjzf4e172\\unknown.31496.43076.opt\".\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 ma57.\n",
+ "\n",
+ "Number of nonzeros in equality constraint Jacobian...: 920\n",
+ "Number of nonzeros in inequality constraint Jacobian.: 0\n",
+ "Number of nonzeros in Lagrangian Hessian.............: 456\n",
+ "\n",
+ "Total number of variables............................: 218\n",
+ " variables with only lower bounds: 56\n",
+ " variables with lower and upper bounds: 155\n",
+ " variables with only upper bounds: 0\n",
+ "Total number of equality constraints.................: 218\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 8.17e+03 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n",
+ "Reallocating memory for MA57: lfact (10193)\n",
+ " 1 0.0000000e+00 7.12e+03 2.01e+02 -1.0 4.35e+04 - 4.68e-02 1.56e-01h 1\n",
+ " 2 0.0000000e+00 7.12e+03 2.74e+02 -1.0 4.20e+05 - 9.82e-04 5.24e-04h 1\n",
+ " 3 0.0000000e+00 7.11e+03 2.06e+03 -1.0 4.26e+05 - 1.43e-05 1.49e-03f 1\n",
+ " 4 0.0000000e+00 7.09e+03 1.83e+03 -1.0 3.65e+04 - 5.78e-03 2.02e-03h 1\n",
+ " 5 0.0000000e+00 7.09e+03 1.82e+03 -1.0 1.61e+05 - 4.69e-04 1.23e-05h 1\n",
+ " 6r 0.0000000e+00 7.09e+03 9.99e+02 3.9 0.00e+00 - 0.00e+00 6.02e-08R 2\n",
+ " 7r 0.0000000e+00 6.68e+03 9.98e+02 3.9 7.11e+06 - 5.22e-04 1.63e-04f 1\n",
+ " 8r 0.0000000e+00 4.74e+04 9.98e+02 3.9 3.89e+06 - 2.84e-04 7.21e-04f 1\n",
+ " 9r 0.0000000e+00 4.77e+04 1.78e+04 3.9 1.73e+06 - 4.34e-02 5.81e-04f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 10r 0.0000000e+00 4.55e+04 4.83e+04 3.2 4.71e+04 - 2.92e-01 4.47e-02f 1\n",
+ " 11r 0.0000000e+00 7.86e+04 3.48e+04 3.2 3.13e+02 - 4.51e-01 3.27e-01f 1\n",
+ " 12r 0.0000000e+00 4.85e+04 2.80e+04 3.2 5.27e+01 - 5.22e-01 2.61e-01f 1\n",
+ " 13r 0.0000000e+00 1.68e+05 1.76e+04 3.2 1.46e+02 - 8.30e-01 4.81e-01f 1\n",
+ " 14r 0.0000000e+00 8.02e+04 3.21e+03 3.2 1.42e+02 - 7.28e-01 1.00e+00f 1\n",
+ " 15r 0.0000000e+00 1.89e+05 6.22e+04 3.2 1.06e+02 - 4.83e-01 1.00e+00f 1\n",
+ " 16r 0.0000000e+00 1.77e+05 5.95e+04 3.2 1.08e+02 0.0 9.89e-02 6.28e-02h 1\n",
+ " 17r 0.0000000e+00 1.03e+05 3.98e+04 3.2 8.09e+01 - 7.18e-01 4.22e-01h 1\n",
+ " 18r 0.0000000e+00 8.88e+04 8.50e+04 3.2 1.59e+02 - 2.14e-01 1.36e-01h 1\n",
+ " 19r 0.0000000e+00 5.30e+04 1.71e+06 3.2 8.69e+01 - 3.02e-02 3.51e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 20r 0.0000000e+00 1.40e+04 2.20e+05 3.2 1.08e+01 2.2 6.72e-01 8.78e-01f 1\n",
+ " 21r 0.0000000e+00 9.61e+03 9.51e+04 2.5 1.06e+01 1.8 9.32e-01 4.27e-01f 1\n",
+ " 22r 0.0000000e+00 1.83e+04 3.37e+04 2.5 1.51e+01 1.3 1.00e+00 6.41e-01f 1\n",
+ " 23r 0.0000000e+00 3.76e+03 1.35e+03 2.5 4.90e+00 0.8 1.00e+00 1.00e+00f 1\n",
+ " 24r 0.0000000e+00 1.38e+05 2.72e+04 2.5 1.06e+02 - 3.87e-01 1.00e+00f 1\n",
+ " 25r 0.0000000e+00 1.06e+05 2.11e+04 2.5 5.11e+01 1.2 2.99e-01 2.35e-01h 1\n",
+ " 26r 0.0000000e+00 9.35e+04 1.83e+04 2.5 3.27e+01 2.6 6.94e-02 1.19e-01h 1\n",
+ " 27r 0.0000000e+00 9.23e+03 3.61e+03 2.5 2.06e+01 2.1 2.35e-01 1.00e+00h 1\n",
+ " 28r 0.0000000e+00 9.21e+03 6.60e+03 2.5 9.05e+02 - 1.37e-01 3.89e-03f 1\n",
+ " 29r 0.0000000e+00 6.69e+04 1.34e+04 2.5 1.58e+02 - 3.39e-01 2.01e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 30r 0.0000000e+00 7.42e+01 3.52e+04 2.5 3.26e-01 1.6 4.89e-01 1.00e+00f 1\n",
+ " 31r 0.0000000e+00 9.36e+03 1.66e+02 2.5 1.29e+01 - 1.00e+00 1.00e+00f 1\n",
+ " 32r 0.0000000e+00 9.30e+01 3.75e+00 2.5 1.85e+00 - 1.00e+00 1.00e+00h 1\n",
+ " 33r 0.0000000e+00 8.97e+03 9.16e+02 0.4 4.59e+01 - 7.14e-01 7.41e-01f 1\n",
+ " 34r 0.0000000e+00 7.19e+03 3.37e+02 0.4 2.34e+03 - 7.23e-01 6.68e-01f 1\n",
+ " 35r 0.0000000e+00 5.64e+03 5.42e+02 0.4 7.33e+02 - 6.81e-01 5.42e-01f 1\n",
+ " 36r 0.0000000e+00 1.10e+03 1.79e+02 0.4 2.38e-01 2.0 6.12e-01 8.67e-01f 1\n",
+ " 37r 0.0000000e+00 3.67e+02 1.23e+02 0.4 9.76e-02 2.5 9.90e-01 8.27e-01f 1\n",
+ " 38r 0.0000000e+00 2.93e+01 3.80e+02 0.4 2.55e-01 2.0 5.41e-01 1.00e+00f 1\n",
+ " 39r 0.0000000e+00 2.58e+01 7.52e+02 0.4 2.70e+00 2.4 4.08e-02 9.27e-02h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 40r 0.0000000e+00 1.03e+01 2.85e+02 0.4 8.74e-02 2.8 7.74e-01 6.78e-01f 1\n",
+ " 41r 0.0000000e+00 4.85e+00 8.16e+01 0.4 2.96e-01 2.4 7.04e-01 7.21e-01f 1\n",
+ " 42r 0.0000000e+00 1.15e+02 7.32e+01 -0.3 1.76e-01 1.9 7.14e-01 7.59e-01f 1\n",
+ " 43r 0.0000000e+00 2.10e+02 3.61e+02 -0.3 4.28e-01 1.4 7.77e-01 1.77e-01f 1\n",
+ " 44r 0.0000000e+00 1.51e+02 6.63e+01 -0.3 4.68e-01 0.9 1.00e+00 8.94e-01f 1\n",
+ " 45r 0.0000000e+00 9.19e+00 1.85e+01 -0.3 1.49e-01 1.3 1.00e+00 1.00e+00f 1\n",
+ " 46r 0.0000000e+00 5.62e+00 5.71e+01 -0.3 1.05e+00 0.9 4.17e-01 3.89e-01h 1\n",
+ " 47r 0.0000000e+00 1.98e+01 3.77e+01 -0.3 7.16e-01 0.4 1.00e+00 1.00e+00f 1\n",
+ " 48r 0.0000000e+00 1.82e+01 6.27e+02 -0.3 9.97e+00 -0.1 4.98e-02 8.93e-02h 1\n",
+ " 49r 0.0000000e+00 1.72e+01 5.84e+02 -0.3 1.90e+00 1.2 4.67e-02 4.96e-02f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 50r 0.0000000e+00 1.16e+01 7.04e+02 -0.3 1.10e+00 0.8 6.53e-01 4.12e-01f 1\n",
+ " 51r 0.0000000e+00 1.08e+01 6.28e+02 -0.3 2.94e-01 2.1 3.66e-01 6.78e-02f 1\n",
+ " 52r 0.0000000e+00 6.69e+00 7.88e+02 -0.3 4.74e-01 1.6 5.14e-01 6.14e-01f 1\n",
+ " 53r 0.0000000e+00 6.34e+00 1.39e+03 -0.3 6.49e-02 2.9 1.00e+00 5.17e-02h 1\n",
+ " 54r 0.0000000e+00 5.30e-01 7.34e+02 -0.3 1.91e-01 2.5 5.47e-01 1.00e+00f 1\n",
+ " 55r 0.0000000e+00 5.30e-01 8.37e+02 -0.3 1.09e-01 2.9 1.00e+00 1.86e-01f 1\n",
+ " 56r 0.0000000e+00 6.24e-01 3.25e+02 -0.3 2.18e-01 2.4 7.22e-01 7.62e-01f 1\n",
+ " 57r 0.0000000e+00 5.84e-01 1.25e+03 -0.3 1.10e-01 2.8 3.44e-01 6.58e-02f 1\n",
+ " 58r 0.0000000e+00 1.84e+00 9.86e+02 -0.3 6.56e-01 2.4 4.76e-01 1.66e-01f 1\n",
+ " 59r 0.0000000e+00 9.98e+00 7.31e+02 -0.3 2.98e-01 1.9 3.81e-01 1.96e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 60r 0.0000000e+00 2.33e+01 6.73e+02 -0.3 2.93e+00 1.4 1.64e-03 2.62e-02f 1\n",
+ " 61r 0.0000000e+00 2.09e+02 6.21e+02 -0.3 3.68e+00 1.8 2.80e-02 4.31e-02f 1\n",
+ " 62r 0.0000000e+00 2.06e+02 4.81e+02 -0.3 4.45e-01 1.4 4.03e-01 1.68e-02h 1\n",
+ " 63r 0.0000000e+00 1.65e+02 3.87e+02 -0.3 1.11e+00 0.9 1.92e-02 1.96e-01f 1\n",
+ " 64r 0.0000000e+00 1.50e+02 3.08e+02 -0.3 1.85e+00 0.4 2.04e-01 2.07e-01f 1\n",
+ " 65r 0.0000000e+00 1.42e+02 2.98e+02 -0.3 1.20e+00 1.7 3.04e-02 5.07e-02f 1\n",
+ " 66r 0.0000000e+00 7.46e+01 5.29e+02 -0.3 5.27e-02 2.2 9.16e-01 4.78e-01f 1\n",
+ " 67r 0.0000000e+00 5.57e+01 4.82e+02 -0.3 1.72e-01 1.7 9.61e-01 2.54e-01f 1\n",
+ " 68r 0.0000000e+00 2.98e+00 1.46e+02 -0.3 2.22e-01 1.2 1.00e+00 9.79e-01f 1\n",
+ " 69r 0.0000000e+00 6.01e+00 2.34e+02 -0.3 4.07e-01 0.7 5.24e-01 4.28e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 70r 0.0000000e+00 1.07e+02 5.44e+02 -0.3 1.11e+00 0.3 7.04e-01 1.00e+00f 1\n",
+ " 71r 0.0000000e+00 2.25e+01 7.39e+02 -0.3 1.39e-01 1.6 3.98e-01 7.90e-01f 1\n",
+ " 72r 0.0000000e+00 2.07e+01 7.10e+02 -0.3 6.94e-01 1.1 2.42e-01 8.16e-02f 1\n",
+ " 73r 0.0000000e+00 4.67e+01 3.79e+02 -0.3 3.86e-01 0.6 6.91e-01 1.00e+00f 1\n",
+ " 74r 0.0000000e+00 7.77e+01 2.65e+02 -0.3 9.27e-01 0.2 4.94e-01 4.38e-01h 1\n",
+ " 75r 0.0000000e+00 7.62e+01 2.61e+02 -0.3 3.81e+00 0.6 2.20e-02 3.06e-02f 2\n",
+ " 76r 0.0000000e+00 5.81e+01 2.16e+02 -0.3 1.57e-01 1.9 3.64e-01 2.38e-01f 1\n",
+ " 77r 0.0000000e+00 1.97e+01 1.33e+02 -0.3 1.61e-01 1.4 1.00e+00 6.92e-01f 1\n",
+ " 78r 0.0000000e+00 4.47e+01 2.11e+02 -0.3 8.61e-01 1.0 7.64e-01 4.57e-01f 1\n",
+ " 79r 0.0000000e+00 2.75e+02 1.68e+02 -0.3 2.89e+00 0.5 1.90e-01 1.97e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 80r 0.0000000e+00 1.57e+01 5.93e+01 -0.3 1.06e-01 1.8 1.00e+00 1.00e+00f 1\n",
+ " 81r 0.0000000e+00 4.52e+00 7.54e+00 -0.3 3.54e-01 1.3 1.00e+00 1.00e+00h 1\n",
+ " 82r 0.0000000e+00 1.51e+01 6.83e+01 -1.0 1.48e-01 1.8 6.09e-01 6.86e-01f 1\n",
+ " 83r 0.0000000e+00 9.47e+00 2.62e+01 -1.0 5.68e-02 2.2 1.00e+00 8.94e-01f 1\n",
+ " 84r 0.0000000e+00 2.30e+01 2.17e+01 -1.0 4.35e-01 1.7 1.62e-01 1.76e-01f 1\n",
+ " 85r 0.0000000e+00 1.67e+01 1.83e+02 -1.0 8.29e-02 2.1 1.00e+00 4.80e-01f 1\n",
+ " 86r 0.0000000e+00 5.83e+01 5.12e+02 -1.0 3.20e-01 1.7 6.90e-01 3.58e-01f 1\n",
+ " 87r 0.0000000e+00 1.07e+02 1.19e+03 -1.0 1.30e+01 1.2 2.99e-02 1.02e-02f 1\n",
+ " 88r 0.0000000e+00 1.05e+02 3.60e+02 -1.0 2.66e+00 0.7 1.00e+00 2.59e-02f 1\n",
+ " 89r 0.0000000e+00 4.13e+02 4.81e+02 -1.0 7.27e+00 0.2 1.00e+00 4.22e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 90r 0.0000000e+00 4.12e+02 4.73e+02 -1.0 7.91e+01 -0.3 2.91e-03 3.88e-03f 1\n",
+ " 91r 0.0000000e+00 3.90e+02 5.09e+02 -1.0 1.05e+00 1.1 1.00e+00 5.42e-02f 1\n",
+ " 92r 0.0000000e+00 6.42e+02 1.96e+01 -1.0 3.03e+00 0.6 1.00e+00 1.00e+00f 1\n",
+ " 93r 0.0000000e+00 1.54e+03 5.81e+02 -1.0 1.25e+01 0.1 9.40e-02 6.11e-01f 1\n",
+ " 94r 0.0000000e+00 1.52e+03 5.61e+02 -1.0 9.43e+00 0.5 1.64e-02 8.32e-03f 1\n",
+ " 95r 0.0000000e+00 1.38e+03 3.41e+02 -1.0 2.32e+01 0.1 3.92e-01 1.27e-01f 1\n",
+ " 96r 0.0000000e+00 1.38e+03 3.72e+02 -1.0 5.95e+00 0.5 3.61e-01 4.34e-03h 1\n",
+ " 97r 0.0000000e+00 1.28e+03 2.01e+02 -1.0 1.09e+01 0.0 4.23e-02 1.05e-01f 1\n",
+ " 98r 0.0000000e+00 9.89e+02 4.20e+02 -1.0 4.94e+00 0.4 3.76e-02 2.66e-01f 1\n",
+ " 99r 0.0000000e+00 8.96e+02 4.65e+02 -1.0 2.96e+00 0.9 2.83e-01 9.51e-02f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 100r 0.0000000e+00 8.38e+02 1.08e+03 -1.0 3.79e+00 0.4 5.07e-01 6.96e-02h 1\n",
+ " 101r 0.0000000e+00 1.19e+03 6.44e+02 -1.0 9.81e+00 -0.1 3.71e-01 3.57e-01f 1\n",
+ " 102r 0.0000000e+00 1.19e+03 5.28e+02 -1.0 1.78e+01 -0.6 7.44e-03 6.07e-02f 1\n",
+ " 103r 0.0000000e+00 1.02e+03 3.34e+02 -1.0 2.10e+01 -1.0 4.17e-02 1.21e-01f 1\n",
+ " 104r 0.0000000e+00 9.98e+02 8.36e+02 -1.0 8.35e+00 -0.6 4.71e-01 1.95e-02h 1\n",
+ " 105r 0.0000000e+00 6.83e+02 6.45e+02 -1.0 1.30e+01 -1.1 5.71e-02 2.84e-01f 1\n",
+ " 106r 0.0000000e+00 8.65e+02 1.14e+03 -1.0 8.25e+00 -0.7 1.61e-01 6.21e-01f 1\n",
+ " 107r 0.0000000e+00 7.16e+02 9.70e+02 -1.0 4.28e+00 -0.2 1.93e-01 1.57e-01h 1\n",
+ " 108r 0.0000000e+00 6.31e+02 8.28e+02 -1.0 6.49e+00 -0.7 6.61e-02 1.81e-01h 1\n",
+ " 109r 0.0000000e+00 4.55e+02 1.19e+03 -1.0 2.95e+00 -0.3 1.88e-01 8.74e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 110r 0.0000000e+00 1.83e+02 4.89e+02 -1.0 1.15e-01 1.9 6.47e-01 5.98e-01h 1\n",
+ " 111r 0.0000000e+00 3.73e+01 1.31e+02 -1.0 8.98e-02 1.5 1.00e+00 7.95e-01h 1\n",
+ " 112r 0.0000000e+00 7.86e+00 8.49e+01 -1.0 7.76e-02 1.9 9.93e-01 7.86e-01h 1\n",
+ " 113r 0.0000000e+00 2.86e+00 1.42e+01 -1.0 1.00e-01 1.4 1.00e+00 1.00e+00h 1\n",
+ " 114r 0.0000000e+00 2.89e+00 2.59e+00 -1.0 3.00e-01 0.9 1.00e+00 1.00e+00f 1\n",
+ " 115r 0.0000000e+00 5.21e+01 6.10e+00 -1.0 8.19e-01 0.5 1.00e+00 1.00e+00f 1\n",
+ " 116r 0.0000000e+00 1.36e+02 4.46e+02 -1.0 2.16e+00 -0.0 2.00e-01 5.43e-01f 1\n",
+ " 117r 0.0000000e+00 2.35e+01 1.35e+02 -1.0 3.81e-02 2.2 6.64e-01 8.26e-01h 1\n",
+ " 118r 0.0000000e+00 1.45e+01 6.04e+02 -1.0 4.14e-01 1.7 1.30e-01 6.39e-01f 1\n",
+ " 119r 0.0000000e+00 7.09e+01 1.51e+03 -1.0 1.49e+00 2.2 9.44e-02 3.42e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 120r 0.0000000e+00 6.91e+01 1.74e+03 -1.0 3.65e-01 2.6 2.02e-01 2.71e-02f 1\n",
+ " 121r 0.0000000e+00 6.89e+01 1.47e+03 -1.0 1.29e+00 2.1 8.23e-04 3.05e-03f 1\n",
+ " 122r 0.0000000e+00 6.81e+01 2.26e+03 -1.0 1.01e+00 1.6 5.80e-02 1.11e-02f 1\n",
+ " 123r 0.0000000e+00 5.53e+01 8.13e+02 -1.0 9.48e-01 1.2 1.55e-02 1.67e-01f 1\n",
+ " 124r 0.0000000e+00 4.39e+01 6.51e+02 -1.0 6.05e-01 0.7 2.20e-01 1.95e-01f 1\n",
+ " 125r 0.0000000e+00 3.95e+01 5.85e+02 -1.0 1.15e+00 0.2 4.00e-01 9.82e-02f 1\n",
+ " 126r 0.0000000e+00 5.31e+01 2.97e+02 -1.0 1.75e+00 -0.3 4.11e-03 3.31e-01f 1\n",
+ " 127r 0.0000000e+00 5.22e+01 2.92e+02 -1.0 1.49e+00 0.2 4.34e-02 1.79e-02h 1\n",
+ " 128r 0.0000000e+00 5.15e+01 2.70e+02 -1.0 9.28e+00 -0.3 1.26e-01 3.54e-02f 1\n",
+ " 129r 0.0000000e+00 2.87e+01 8.41e+01 -1.0 1.20e+00 0.1 9.87e-01 7.24e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 130r 0.0000000e+00 5.74e+01 7.12e+01 -1.0 2.02e+00 -0.4 1.00e+00 3.08e-01h 1\n",
+ " 131r 0.0000000e+00 1.40e+03 2.57e+01 -1.0 6.81e+00 -0.9 6.75e-01 8.23e-01f 1\n",
+ " 132r 0.0000000e+00 1.35e+03 2.54e+01 -1.0 1.62e+01 -1.3 1.03e-01 3.34e-02h 1\n",
+ " 133r 0.0000000e+00 1.23e+03 3.95e+01 -1.0 7.83e+00 -0.9 7.40e-01 6.60e-01h 1\n",
+ " 134r 0.0000000e+00 1.19e+03 3.30e+02 -1.0 1.81e+01 -1.4 3.93e-01 5.73e-02h 1\n",
+ " 135r 0.0000000e+00 1.18e+03 3.16e+02 -1.0 1.12e+02 -1.9 2.06e-02 2.35e-02h 1\n",
+ " 136r 0.0000000e+00 3.04e+03 1.74e+02 -1.0 1.66e+01 -1.4 6.20e-01 5.67e-01h 1\n",
+ " 137r 0.0000000e+00 3.10e+03 7.86e+01 -1.0 4.18e+02 -1.9 4.32e-04 5.13e-03f 1\n",
+ " 138r 0.0000000e+00 3.15e+03 8.59e+01 -1.0 2.08e+01 -1.5 6.10e-01 3.20e-01f 1\n",
+ " 139r 0.0000000e+00 6.79e+03 9.92e+01 -1.0 6.29e+01 -2.0 5.13e-01 5.43e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 140r 0.0000000e+00 1.99e+03 2.93e+02 -1.0 1.82e-01 1.2 9.52e-01 7.07e-01h 1\n",
+ " 141r 0.0000000e+00 4.14e-01 6.19e+01 -1.0 4.03e-02 1.6 9.05e-01 1.00e+00h 1\n",
+ " 142r 0.0000000e+00 5.40e-01 1.00e+01 -1.0 7.66e-02 1.1 1.00e+00 1.00e+00h 1\n",
+ " 143r 0.0000000e+00 1.87e+00 7.98e-01 -1.0 1.80e-01 0.6 1.00e+00 1.00e+00h 1\n",
+ " 144r 0.0000000e+00 1.04e+01 9.40e-01 -1.0 4.36e-01 0.2 1.00e+00 1.00e+00h 1\n",
+ " 145r 0.0000000e+00 3.19e+01 4.06e+00 -1.0 9.89e-01 -0.3 1.00e+00 1.00e+00h 1\n",
+ " 146r 0.0000000e+00 2.28e+02 6.27e+00 -1.0 3.02e+00 -0.8 1.00e+00 9.35e-01h 1\n",
+ " 147r 0.0000000e+00 7.58e+02 1.93e+02 -1.0 1.17e+01 -1.3 1.00e+00 5.43e-01h 1\n",
+ " 148r 0.0000000e+00 7.62e+02 6.49e+02 -1.0 5.17e+01 -1.7 4.76e-01 9.10e-02h 1\n",
+ " 149r 0.0000000e+00 5.18e+03 4.98e+02 -1.0 3.35e+02 -2.2 2.78e-02 1.43e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 150r 0.0000000e+00 5.19e+03 7.41e+02 -1.0 1.23e+03 -2.7 1.42e-02 2.95e-02f 1\n",
+ " 151r 0.0000000e+00 6.87e+03 9.57e+02 -1.0 1.12e+03 -2.3 1.35e-03 3.17e-02f 1\n",
+ " 152r 0.0000000e+00 6.85e+03 8.50e+02 -1.0 1.37e+02 -1.8 3.99e-01 4.06e-03h 1\n",
+ " 153r 0.0000000e+00 6.72e+03 9.57e+02 -1.0 4.39e+02 -2.3 1.18e-01 1.81e-02h 1\n",
+ " 154r 0.0000000e+00 6.40e+03 1.13e+03 -1.0 1.07e+04 - 1.49e-01 4.89e-02f 1\n",
+ " 155r 0.0000000e+00 5.62e+03 9.92e+02 -1.0 9.94e+03 - 1.20e-01 1.22e-01f 1\n",
+ " 156r 0.0000000e+00 5.58e+03 9.64e+02 -1.0 3.35e+03 - 1.73e-03 7.88e-03h 1\n",
+ " 157r 0.0000000e+00 5.58e+03 9.64e+02 -1.0 2.55e+05 -2.8 2.32e-05 2.33e-05f 1\n",
+ " 158r 0.0000000e+00 5.55e+03 1.02e+03 -1.0 4.99e+02 -3.3 2.90e-02 5.00e-03h 1\n",
+ " 159r 0.0000000e+00 4.91e+03 9.36e+02 -1.0 1.06e+02 -3.8 1.63e-01 1.16e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 160r 0.0000000e+00 4.19e+03 7.63e+02 -1.0 1.75e+02 -2.4 1.15e-02 1.48e-01h 1\n",
+ " 161r 0.0000000e+00 4.18e+03 7.94e+02 -1.0 4.34e+02 -2.9 3.97e-02 1.46e-03h 1\n",
+ " 162r 0.0000000e+00 2.31e+03 3.53e+02 -1.0 5.33e+03 - 1.01e-01 4.63e-01h 1\n",
+ " 163r 0.0000000e+00 2.04e+03 5.97e+02 -1.0 3.18e+03 - 1.73e-01 1.20e-01h 1\n",
+ " 164r 0.0000000e+00 1.26e+03 3.46e+02 -1.0 3.03e+03 - 4.18e-01 4.11e-01h 1\n",
+ " 165r 0.0000000e+00 1.24e+03 3.20e+02 -1.0 2.32e+03 - 1.97e-03 1.44e-02h 1\n",
+ " 166r 0.0000000e+00 1.17e+03 4.22e+02 -1.0 2.31e+03 - 3.37e-01 5.42e-02h 1\n",
+ " 167r 0.0000000e+00 1.04e+03 6.87e+02 -1.0 1.74e+03 - 9.08e-01 1.16e-01h 1\n",
+ " 168r 0.0000000e+00 3.09e+02 4.10e+02 -1.0 1.64e+03 - 4.91e-01 8.06e-01h 1\n",
+ " 169r 0.0000000e+00 3.95e+02 5.59e+02 -1.0 2.46e+03 - 1.49e-02 2.83e-02f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 170r 0.0000000e+00 2.99e+02 7.69e+01 -1.0 4.31e+02 - 8.88e-01 1.00e+00f 1\n",
+ " 171r 0.0000000e+00 1.65e+02 3.84e+02 -1.0 4.91e-01 0.2 1.00e+00 4.50e-01h 1\n",
+ " 172r 0.0000000e+00 1.87e-01 5.61e+01 -1.0 2.62e+00 - 1.00e+00 1.00e+00h 1\n",
+ " 173r 0.0000000e+00 1.87e-01 2.09e+01 -1.0 8.62e-01 - 1.00e+00 1.00e+00h 1\n",
+ " 174r 0.0000000e+00 1.87e-01 1.77e+00 -1.0 9.94e-02 - 1.00e+00 1.00e+00h 1\n",
+ " 175r 0.0000000e+00 7.96e+00 5.39e+00 -1.7 1.21e+01 - 1.00e+00 9.98e-01f 1\n",
+ " 176r 0.0000000e+00 2.31e+02 1.33e+02 -1.7 1.26e+04 - 3.42e-01 1.24e-01f 1\n",
+ " 177r 0.0000000e+00 2.38e+02 3.38e+02 -1.7 1.05e+04 - 6.40e-01 3.94e-02f 1\n",
+ " 178r 0.0000000e+00 2.37e+02 3.35e+02 -1.7 7.28e+03 - 9.97e-02 1.97e-03h 1\n",
+ " 179r 0.0000000e+00 2.11e+02 3.90e+02 -1.7 3.85e+03 - 5.39e-01 1.07e-01h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 180r 0.0000000e+00 3.16e+02 4.17e+02 -1.7 3.82e+03 - 1.28e-02 9.51e-01f 1\n",
+ " 181r 0.0000000e+00 1.07e+03 2.19e+02 -1.7 3.96e+03 - 4.42e-01 4.76e-01h 1\n",
+ " 182r 0.0000000e+00 1.05e+03 2.05e+02 -1.7 1.41e+03 - 4.64e-01 2.04e-02h 1\n",
+ " 183r 0.0000000e+00 4.56e+02 1.83e+02 -1.7 1.51e+00 -0.2 4.60e-01 5.66e-01h 1\n",
+ " 184r 0.0000000e+00 4.46e+02 4.37e+02 -1.7 1.26e+03 - 5.85e-01 2.25e-02h 1\n",
+ " 185r 0.0000000e+00 3.62e+02 5.95e+02 -1.7 1.17e+03 - 3.15e-01 2.25e-01h 1\n",
+ " 186r 0.0000000e+00 3.47e+02 2.11e+02 -1.7 6.54e-01 -0.7 4.90e-01 4.12e-02h 1\n",
+ " 187r 0.0000000e+00 2.68e+02 4.10e+02 -1.7 9.73e+02 - 1.74e-01 2.84e-01h 1\n",
+ " 188r 0.0000000e+00 2.32e+02 7.23e+02 -1.7 1.37e+03 - 7.54e-01 3.83e-01h 1\n",
+ " 189r 0.0000000e+00 2.25e+02 1.95e+02 -1.7 7.05e+02 - 6.62e-01 3.01e-02h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 190r 0.0000000e+00 1.61e+02 3.01e+02 -1.7 4.09e+02 - 1.00e+00 3.03e-01h 1\n",
+ " 191r 0.0000000e+00 9.61e-01 6.13e-01 -1.7 6.25e+01 - 1.00e+00 1.00e+00h 1\n",
+ " 192r 0.0000000e+00 5.56e-02 3.83e-02 -1.7 6.74e+00 - 1.00e+00 1.00e+00h 1\n",
+ " 193r 0.0000000e+00 3.80e-01 2.41e+01 -3.9 2.90e+01 - 9.11e-01 7.83e-01f 1\n",
+ " 194r 0.0000000e+00 2.60e+00 2.67e+01 -3.9 4.29e+04 - 7.72e-02 5.58e-02f 1\n",
+ " 195r 0.0000000e+00 2.60e+00 3.11e+02 -3.9 2.37e+04 - 6.85e-02 1.39e-05h 1\n",
+ " 196r 0.0000000e+00 2.91e+00 2.90e+02 -3.9 3.11e+03 - 1.53e-01 3.55e-02f 1\n",
+ " 197r 0.0000000e+00 1.00e+01 2.50e+02 -3.9 2.55e+03 - 1.71e-01 1.67e-01f 1\n",
+ " 198r 0.0000000e+00 2.87e+01 3.43e+02 -3.9 2.12e+03 - 7.25e-02 3.31e-01f 1\n",
+ " 199r 0.0000000e+00 4.82e+01 9.78e+02 -3.9 1.55e+03 - 5.38e-02 5.45e-01f 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 200r 0.0000000e+00 1.40e+02 6.78e+02 -3.9 3.21e+04 - 2.94e-02 1.88e-01f 1\n",
+ " 201r 0.0000000e+00 1.38e+02 6.81e+02 -3.9 2.53e+04 - 4.87e-02 1.94e-02h 1\n",
+ " 202r 0.0000000e+00 1.43e+02 6.07e+02 -3.9 2.53e+04 - 2.36e-02 7.13e-02f 1\n",
+ " 203r 0.0000000e+00 2.17e+02 4.73e+02 -3.9 2.36e+04 - 9.43e-02 1.98e-01f 1\n",
+ " 204r 0.0000000e+00 2.11e+02 5.82e+02 -3.9 1.89e+04 - 5.56e-01 6.52e-02h 1\n",
+ " 205r 0.0000000e+00 2.11e+02 7.24e+02 -3.9 1.33e+04 - 8.62e-01 3.50e-03h 1\n",
+ " 206r 0.0000000e+00 1.88e+02 8.64e+02 -3.9 3.55e+02 - 1.00e+00 1.09e-01h 1\n",
+ " 207r 0.0000000e+00 3.04e+01 5.52e+02 -3.9 3.16e+02 - 2.11e-01 8.62e-01h 1\n",
+ " 208r 0.0000000e+00 7.41e-01 1.91e+00 -3.9 4.38e+01 - 1.00e+00 9.97e-01h 1\n",
+ " 209r 0.0000000e+00 7.13e-01 6.42e+02 -3.9 1.50e-01 - 7.36e-02 3.78e-02h 1\n",
+ "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n",
+ " 210r 0.0000000e+00 5.37e-03 4.03e+02 -3.9 1.44e-01 - 5.86e-01 1.00e+00h 1\n",
+ " 211r 0.0000000e+00 5.37e-03 2.68e-04 -3.9 2.52e-03 - 1.00e+00 1.00e+00h 1\n",
+ " 212r 0.0000000e+00 5.37e-03 5.94e+01 -5.9 3.33e-01 - 1.00e+00 6.67e-01f 1\n",
+ " 213r 0.0000000e+00 4.67e+01 1.51e+02 -5.9 2.60e+04 - 1.03e-01 2.28e-02f 1\n",
+ " 214r 0.0000000e+00 4.67e+01 8.78e+02 -5.9 4.07e+02 - 8.75e-01 9.73e-07h 2\n",
+ " 215r 0.0000000e+00 1.68e+01 2.98e+02 -5.9 1.59e-01 - 8.59e-01 6.40e-01h 1\n",
+ " 216r 0.0000000e+00 2.59e+00 5.90e+01 -5.9 3.25e-02 - 1.00e+00 8.46e-01h 1\n",
+ " 217r 0.0000000e+00 1.57e-04 3.42e-04 -5.9 3.65e-03 - 1.00e+00 1.00e+00h 1\n",
+ " 218r 0.0000000e+00 2.69e-09 6.22e-06 -5.9 2.22e-04 - 1.00e+00 1.00e+00h 1\n",
+ "\n",
+ "Number of Iterations....: 218\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.6943874215090702e-09 2.6943874215090702e-09\n",
+ "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n",
+ "Overall NLP error.......: 2.6943874215090702e-09 2.6943874215090702e-09\n",
+ "\n",
+ "\n",
+ "Number of objective function evaluations = 225\n",
+ "Number of objective gradient evaluations = 8\n",
+ "Number of equality constraint evaluations = 225\n",
+ "Number of inequality constraint evaluations = 0\n",
+ "Number of equality constraint Jacobian evaluations = 220\n",
+ "Number of inequality constraint Jacobian evaluations = 0\n",
+ "Number of Lagrangian Hessian evaluations = 218\n",
+ "Total CPU secs in IPOPT (w/o function evaluations) = 0.347\n",
+ "Total CPU secs in NLP function evaluations = 0.036\n",
+ "\n",
+ "EXIT: Optimal Solution Found.\n"
+ ]
+ }
+ ],
+ "execution_count": 46
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 7 Analyze the results\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If the IDAES UI package was installed with the `idaes-pse` installation or installed separately, you can run the flowsheet visualizer to see a full diagram of the full process that is generated and displayed on a browser window.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Otherwise, we can run the `m.fs.report()` method to see a full summary of the solved flowsheet. It is recommended to adjust the width of the output as much as possible for the cleanest display."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.046845800Z",
+ "start_time": "2026-01-13T00:12:33.009050800Z"
+ }
+ },
+ "source": [
+ "m.fs.report()"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "====================================================================================\n",
+ "Flowsheet : fs Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units s01 s02 s03 s04 s05 s06 s07 s08 s09 s10 s11 s12 \n",
+ " Total Molar Flowrate Liq mole / second 0.30001 2.0000e-05 0.34190 1.6073e-09 5.7340e-09 1.0000e-08 0.26712 1.1139e-06 1.1143e-06 1.0000e-08 0.094878 2.7856e-07\n",
+ " Total Molar Flowrate Vap mole / second 4.0000e-05 0.32002 1.6901 2.0320 2.0320 1.7648 1.0000e-08 1.4119 1.4119 0.17224 1.0000e-08 0.35297\n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 3.3332e-05 0.50000 0.22733 0.13374 0.63390 0.76595 0.76595 0.76595 0.76595 0.66001 0.66001 0.76595\n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.99997 0.50000 0.77267 0.86626 0.36610 0.23405 0.23405 0.23405 0.23405 0.33999 0.33999 0.23405\n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.25000 3.1248e-05 0.024624 0.058732 0.17408 0.084499 0.084499 0.084499 0.084499 0.82430 0.82430 0.084499\n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.25000 3.1248e-05 0.028601 0.15380 0.038450 0.0088437 0.0088437 0.0088435 0.0088435 0.17570 0.17570 0.0088435\n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.25000 0.93744 0.33283 0.27683 0.16148 0.18592 0.18592 0.18592 0.18592 1.7561e-08 1.7561e-08 0.18592\n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.25000 0.062496 0.61394 0.51064 0.62599 0.72074 0.72074 0.72074 0.72074 4.3265e-08 4.3265e-08 0.72074\n",
+ " Temperature kelvin 303.20 303.20 324.51 600.00 771.86 325.00 325.00 325.00 325.00 375.00 375.00 325.00\n",
+ " Pressure pascal 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 1.5000e+05 1.5000e+05 3.5000e+05\n",
+ "====================================================================================\n"
+ ]
+ }
+ ],
+ "execution_count": 49
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "What is the total operating cost?"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.065372900Z",
+ "start_time": "2026-01-13T00:12:33.055710300Z"
+ }
+ },
+ "source": [
+ "print(\"operating cost = $\", value(m.fs.operating_cost))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "operating cost = $ 419008.281895999\n"
+ ]
+ }
+ ],
+ "execution_count": 50
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For this operating cost, what is the amount of benzene we are able to produce and what purity we are able to achieve? We can look at a specific unit models stream table with the same `report()` method."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.179064200Z",
+ "start_time": "2026-01-13T00:12:33.163247500Z"
+ }
+ },
+ "source": [
+ "m.fs.F102.report()\n",
+ "\n",
+ "print()\n",
+ "print(\"benzene purity = \", value(m.fs.purity))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.F102 Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : 7346.7 : watt : False : (None, None)\n",
+ " Pressure Change : -2.0000e+05 : pascal : True : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " Total Molar Flowrate Liq mole / second 0.26712 - - \n",
+ " Total Molar Flowrate Vap mole / second 1.0000e-08 - - \n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 0.76595 - - \n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.23405 - - \n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.084499 - - \n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.0088437 - - \n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.18592 - - \n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.72074 - - \n",
+ " Temperature kelvin 325.00 - - \n",
+ " Pressure pascal 3.5000e+05 - - \n",
+ " flow_mol_phase Liq mole / second - 1.0000e-08 0.094878 \n",
+ " flow_mol_phase Vap mole / second - 0.17224 1.0000e-08 \n",
+ " mole_frac_phase_comp ('Liq', 'benzene') dimensionless - 0.66001 0.66001 \n",
+ " mole_frac_phase_comp ('Liq', 'toluene') dimensionless - 0.33999 0.33999 \n",
+ " mole_frac_phase_comp ('Vap', 'benzene') dimensionless - 0.82430 0.82430 \n",
+ " mole_frac_phase_comp ('Vap', 'toluene') dimensionless - 0.17570 0.17570 \n",
+ " mole_frac_phase_comp ('Vap', 'hydrogen') dimensionless - 1.7561e-08 1.7561e-08 \n",
+ " mole_frac_phase_comp ('Vap', 'methane') dimensionless - 4.3265e-08 4.3265e-08 \n",
+ " temperature kelvin - 375.00 375.00 \n",
+ " pressure pascal - 1.5000e+05 1.5000e+05 \n",
+ "====================================================================================\n",
+ "\n",
+ "benzene purity = 0.8242963450787166\n"
+ ]
+ }
+ ],
+ "execution_count": 52
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Next, let's look at how much benzene we are losing with the light gases out of F101. IDAES has tools for creating stream tables based on the `Arcs` and/or `Ports` in a flowsheet. Let us create and print a simple stream table showing the stream leaving the reactor and the vapor stream from F101."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.290365100Z",
+ "start_time": "2026-01-13T00:12:33.276957500Z"
+ }
+ },
+ "source": [
+ "from idaes.core.util.tables import (\n",
+ " create_stream_table_dataframe,\n",
+ " stream_table_dataframe_to_string,\n",
+ ")\n",
+ "\n",
+ "st = create_stream_table_dataframe({\"Reactor\": m.fs.s05, \"Light Gases\": m.fs.s06})\n",
+ "print(stream_table_dataframe_to_string(st))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " Units Reactor Light Gases\n",
+ "Total Molar Flowrate Liq mole / second 5.7340e-09 1.0000e-08 \n",
+ "Total Molar Flowrate Vap mole / second 2.0320 1.7648 \n",
+ "Total Mole Fraction ('Liq', 'benzene') dimensionless 0.63390 0.76595 \n",
+ "Total Mole Fraction ('Liq', 'toluene') dimensionless 0.36610 0.23405 \n",
+ "Total Mole Fraction ('Vap', 'benzene') dimensionless 0.17408 0.084499 \n",
+ "Total Mole Fraction ('Vap', 'toluene') dimensionless 0.038450 0.0088437 \n",
+ "Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.16148 0.18592 \n",
+ "Total Mole Fraction ('Vap', 'methane') dimensionless 0.62599 0.72074 \n",
+ "Temperature kelvin 771.86 325.00 \n",
+ "Pressure pascal 3.5000e+05 3.5000e+05 \n"
+ ]
+ }
+ ],
+ "execution_count": 54
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 8 Optimization\n",
+ "\n",
+ "\n",
+ "We saw from the results above that the total operating cost for the base case was $419,122 per year. We are producing 0.142 mol/s of benzene at a purity of 82\\%. However, we are losing around 42\\% of benzene in F101 vapor outlet stream. \n",
+ "\n",
+ "Let us try to minimize this cost such that:\n",
+ "- we are producing at least 0.15 mol/s of benzene in F102 vapor outlet i.e. our product stream\n",
+ "- purity of benzene i.e. the mole fraction of benzene in F102 vapor outlet is at least 80%\n",
+ "- restricting the benzene loss in F101 vapor outlet to less than 20%\n",
+ "\n",
+ "For this problem, our decision variables are as follows:\n",
+ "- H101 outlet temperature\n",
+ "- R101 cooling duty provided\n",
+ "- F101 outlet temperature\n",
+ "- F102 outlet temperature\n",
+ "- F102 deltaP in the flash tank\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us declare our objective function for this problem. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.307079900Z",
+ "start_time": "2026-01-13T00:12:33.298304Z"
+ }
+ },
+ "source": [
+ "m.fs.objective = Objective(expr=m.fs.operating_cost)"
+ ],
+ "outputs": [],
+ "execution_count": 55
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, we need to unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.315754900Z",
+ "start_time": "2026-01-13T00:12:33.308100100Z"
+ }
+ },
+ "source": [
+ "m.fs.H101.outlet.temperature.unfix()\n",
+ "m.fs.R101.heat_duty.unfix()\n",
+ "m.fs.F101.vap_outlet.temperature.unfix()\n",
+ "m.fs.F102.vap_outlet.temperature.unfix()"
+ ],
+ "outputs": [],
+ "execution_count": 56
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "solution"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.333132300Z",
+ "start_time": "2026-01-13T00:12:33.324956200Z"
+ }
+ },
+ "source": [
+ "# Todo: Unfix deltaP for F102\n",
+ "m.fs.F102.deltaP.unfix()"
+ ],
+ "outputs": [],
+ "execution_count": 58
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Next, we need to set bounds on these decision variables to values shown below:\n",
+ "\n",
+ " - H101 outlet temperature [500, 600] K\n",
+ " - R101 outlet temperature [600, 800] K\n",
+ " - F101 outlet temperature [298, 450] K\n",
+ " - F102 outlet temperature [298, 450] K\n",
+ " - F102 outlet pressure [105000, 110000] Pa\n",
+ "\n",
+ "Let us first set the variable bound for the H101 outlet temperature as shown below:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.371189900Z",
+ "start_time": "2026-01-13T00:12:33.362714700Z"
+ }
+ },
+ "source": [
+ "m.fs.H101.outlet.temperature[0].setlb(500)\n",
+ "m.fs.H101.outlet.temperature[0].setub(600)"
+ ],
+ "outputs": [],
+ "execution_count": 60
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "solution"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.406097200Z",
+ "start_time": "2026-01-13T00:12:33.395647Z"
+ }
+ },
+ "source": [
+ "# Todo: Set the bounds for reactor outlet temperature\n",
+ "m.fs.R101.outlet.temperature[0].setlb(600)\n",
+ "m.fs.R101.outlet.temperature[0].setub(800)"
+ ],
+ "outputs": [],
+ "execution_count": 62
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us fix the bounds for the rest of the decision variables. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.426888400Z",
+ "start_time": "2026-01-13T00:12:33.413693900Z"
+ }
+ },
+ "source": [
+ "m.fs.F101.vap_outlet.temperature[0].setlb(298.0)\n",
+ "m.fs.F101.vap_outlet.temperature[0].setub(450.0)\n",
+ "m.fs.F102.vap_outlet.temperature[0].setlb(298.0)\n",
+ "m.fs.F102.vap_outlet.temperature[0].setub(450.0)\n",
+ "m.fs.F102.vap_outlet.pressure[0].setlb(105000)\n",
+ "m.fs.F102.vap_outlet.pressure[0].setub(110000)"
+ ],
+ "outputs": [],
+ "execution_count": 63
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, the only things left to define are our constraints on overhead loss in F101, product flow rate and purity in F102. Let us first look at defining a constraint for the overhead loss in F101 where we are restricting the benzene leaving the vapor stream to less than 20 \\% of the benzene available in the reactor outlet. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.447188200Z",
+ "start_time": "2026-01-13T00:12:33.436145800Z"
+ }
+ },
+ "source": [
+ "m.fs.overhead_loss = Constraint(\n",
+ " expr=m.fs.F101.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"benzene\"\n",
+ " ]\n",
+ " <= 0.20\n",
+ " * m.fs.R101.control_volume.properties_out[0].flow_mol_phase_comp[\"Vap\", \"benzene\"]\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 64
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "solution"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.481454400Z",
+ "start_time": "2026-01-13T00:12:33.471763500Z"
+ }
+ },
+ "source": [
+ "# Todo: Add minimum product flow constraint\n",
+ "m.fs.product_flow = Constraint(\n",
+ " expr=m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"benzene\"\n",
+ " ]\n",
+ " >= 0.15\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 66
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us add the final constraint on product purity or the mole fraction of benzene in the product stream such that it is at least greater than 80%. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.500096Z",
+ "start_time": "2026-01-13T00:12:33.487986500Z"
+ }
+ },
+ "source": [
+ "m.fs.product_purity = Constraint(expr=m.fs.purity >= 0.80)"
+ ],
+ "outputs": [],
+ "execution_count": 67
+ },
+ {
+ "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",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.630996900Z",
+ "start_time": "2026-01-13T00:12:33.501556Z"
+ }
+ },
+ "source": "results = solver.solve(m, tee=False)",
+ "outputs": [],
+ "execution_count": 68
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 8.1 Optimization Results\n",
+ "\n",
+ "Display the results and product specifications"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.700975700Z",
+ "start_time": "2026-01-13T00:12:33.660461600Z"
+ }
+ },
+ "source": [
+ "print(\"operating cost = $\", value(m.fs.operating_cost))\n",
+ "\n",
+ "print()\n",
+ "print(\"Product flow rate and purity in F102\")\n",
+ "\n",
+ "m.fs.F102.report()\n",
+ "\n",
+ "print()\n",
+ "print(\"benzene purity = \", value(m.fs.purity))\n",
+ "\n",
+ "print()\n",
+ "print(\"Overhead loss in F101\")\n",
+ "m.fs.F101.report()"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "operating cost = $ 312674.2367537996\n",
+ "\n",
+ "Product flow rate and purity in F102\n",
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.F102 Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : 8370.2 : watt : False : (None, None)\n",
+ " Pressure Change : -2.4500e+05 : pascal : False : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " Total Molar Flowrate Liq mole / second 0.28812 - - \n",
+ " Total Molar Flowrate Vap mole / second 1.0000e-08 - - \n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 0.75463 - - \n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.24537 - - \n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.032748 - - \n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.0032478 - - \n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.21614 - - \n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.74786 - - \n",
+ " Temperature kelvin 301.88 - - \n",
+ " Pressure pascal 3.5000e+05 - - \n",
+ " flow_mol_phase Liq mole / second - 1.0000e-08 0.10493 \n",
+ " flow_mol_phase Vap mole / second - 0.18319 1.0000e-08 \n",
+ " mole_frac_phase_comp ('Liq', 'benzene') dimensionless - 0.64256 0.64256 \n",
+ " mole_frac_phase_comp ('Liq', 'toluene') dimensionless - 0.35744 0.35744 \n",
+ " mole_frac_phase_comp ('Vap', 'benzene') dimensionless - 0.81883 0.81883 \n",
+ " mole_frac_phase_comp ('Vap', 'toluene') dimensionless - 0.18117 0.18117 \n",
+ " mole_frac_phase_comp ('Vap', 'hydrogen') dimensionless - 1.1799e-08 1.1799e-08 \n",
+ " mole_frac_phase_comp ('Vap', 'methane') dimensionless - 4.0825e-08 4.0825e-08 \n",
+ " temperature kelvin - 362.93 362.93 \n",
+ " pressure pascal - 1.0500e+05 1.0500e+05 \n",
+ "====================================================================================\n",
+ "\n",
+ "benzene purity = 0.8188295888412465\n",
+ "\n",
+ "Overhead loss in F101\n",
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.F101 Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : -68347. : watt : False : (None, None)\n",
+ " Pressure Change : 0.0000 : pascal : True : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " Total Molar Flowrate Liq mole / second 1.5819e-12 - - \n",
+ " Total Molar Flowrate Vap mole / second 1.9480 - - \n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 0.57381 - - \n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.42619 - - \n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.13952 - - \n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.039059 - - \n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.18417 - - \n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.63725 - - \n",
+ " Temperature kelvin 775.95 - - \n",
+ " Pressure pascal 3.5000e+05 - - \n",
+ " flow_mol_phase Liq mole / second - 1.0000e-08 0.28812 \n",
+ " flow_mol_phase Vap mole / second - 1.6598 1.0000e-08 \n",
+ " mole_frac_phase_comp ('Liq', 'benzene') dimensionless - 0.75463 0.75463 \n",
+ " mole_frac_phase_comp ('Liq', 'toluene') dimensionless - 0.24537 0.24537 \n",
+ " mole_frac_phase_comp ('Vap', 'benzene') dimensionless - 0.032748 0.032748 \n",
+ " mole_frac_phase_comp ('Vap', 'toluene') dimensionless - 0.0032478 0.0032478 \n",
+ " mole_frac_phase_comp ('Vap', 'hydrogen') dimensionless - 0.21614 0.21614 \n",
+ " mole_frac_phase_comp ('Vap', 'methane') dimensionless - 0.74786 0.74786 \n",
+ " temperature kelvin - 301.88 301.88 \n",
+ " pressure pascal - 3.5000e+05 3.5000e+05 \n",
+ "====================================================================================\n"
+ ]
+ }
+ ],
+ "execution_count": 70
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Display optimal values for the decision variables"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.735319500Z",
+ "start_time": "2026-01-13T00:12:33.718148400Z"
+ }
+ },
+ "source": [
+ "print(\n",
+ " f\"\"\"Optimal Values:\n",
+ "\n",
+ "H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):.3f} K\n",
+ "\n",
+ "R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):.3f} K\n",
+ "\n",
+ "F101 outlet temperature = {value(m.fs.F101.vap_outlet.temperature[0]):.3f} K\n",
+ "\n",
+ "F102 outlet temperature = {value(m.fs.F102.vap_outlet.temperature[0]):.3f} K\n",
+ "F102 outlet pressure = {value(m.fs.F102.vap_outlet.pressure[0]):.3f} Pa\n",
+ "\"\"\"\n",
+ ")"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Optimal Values:\n",
+ "\n",
+ "H101 outlet temperature = 500.000 K\n",
+ "\n",
+ "R101 outlet temperature = 775.947 K\n",
+ "\n",
+ "F101 outlet temperature = 301.881 K\n",
+ "\n",
+ "F102 outlet temperature = 362.935 K\n",
+ "F102 outlet pressure = 105000.000 Pa\n",
+ "\n"
+ ]
+ }
+ ],
+ "execution_count": 72
+ }
+ ],
+ "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.12.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 3
}
\ No newline at end of file
diff --git a/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_exercise.ipynb b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_exercise.ipynb
index ef8abc44..727c7192 100644
--- a/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_exercise.ipynb
+++ b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_exercise.ipynb
@@ -1,1344 +1,2011 @@
{
- "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",
- "# HDA Flowsheet Simulation and Optimization\n",
- "\n",
- "Author: Jaffer Ghouse \n",
- "Maintainer: Brandon Paul \n",
- "Updated: 2023-06-01 \n",
- "\n",
- "## Learning outcomes\n",
- "\n",
- "\n",
- "- Construct a steady-state flowsheet using the IDAES unit model library\n",
- "- Connecting unit models in a flowsheet using Arcs\n",
- "- Using the SequentialDecomposition tool to initialize a flowsheet with recycle\n",
- "- Formulate 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",
- "Hydrodealkylation is a chemical reaction that often involves reacting\n",
- "an aromatic hydrocarbon in the presence of hydrogen gas to form a\n",
- "simpler aromatic hydrocarbon devoid of functional groups. In this\n",
- "example, toluene will be reacted with hydrogen gas at high temperatures\n",
- " to form benzene via the following reaction:\n",
- "\n",
- "**C
6H
5CH
3 + H
2 → C
6H
6 + CH
4**\n",
- "\n",
- "\n",
- "This reaction is often accompanied by an equilibrium side reaction\n",
- "which forms diphenyl, which we will neglect for this example.\n",
- "\n",
- "This example is based on the 1967 AIChE Student Contest problem as\n",
- "present by Douglas, J.M., Chemical Design of Chemical Processes, 1988,\n",
- "McGraw-Hill.\n",
- "\n",
- "The flowsheet that we will be using for this module is shown below with the stream conditions. We will be processing toluene and hydrogen to produce at least 370 TPY of benzene. As shown in the flowsheet, there are two flash tanks, F101 to separate out the non-condensibles and F102 to further separate the benzene-toluene mixture to improve the benzene purity. Note that typically a distillation column is required to obtain high purity benzene but that is beyond the scope of this workshop. The non-condensibles separated out in F101 will be partially recycled back to M101 and the rest will be either purged or combusted for power generation.We will assume ideal gas for this flowsheet. The properties required for this module are available in the same directory:\n",
- "\n",
- "- hda_ideal_VLE.py\n",
- "- hda_reaction.py\n",
- "\n",
- "The state variables chosen for the property package are **flows of component by phase, temperature and pressure**. The components considered are: **toluene, hydrogen, benzene and methane**. Therefore, every stream has 8 flow variables, 1 temperature and 1 pressure variable. \n",
- "\n",
- "\n",
- "\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 package. 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",
- "- SolverFactory (to solve the problem)\n",
- "- TransformationFactory (to apply certain transformations)\n",
- "- Arc (to connect two unit models)\n",
- "- SequentialDecomposition (to initialize the flowsheet in a sequential mode)\n",
- "\n",
- "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\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",
- " SolverFactory,\n",
- " TransformationFactory,\n",
- " value,\n",
- ")\n",
- "from pyomo.network import Arc, SequentialDecomposition"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "From idaes, we will be needing the FlowsheetBlock and the following unit models:\n",
- "- Mixer\n",
- "- Heater\n",
- "- StoichiometricReactor\n",
- "-
**Flash**\n",
- "- Separator (splitter) \n",
- "- PressureChanger"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.core import FlowsheetBlock"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.models.unit_models import (\n",
- " PressureChanger,\n",
- " Mixer,\n",
- " Separator as Splitter,\n",
- " Heater,\n",
- " StoichiometricReactor,\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "Now, import the remaining unit models highlighted in blue above and run the cell using `Shift+Enter` after typing in the code. \n",
- "
\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: import flash model from idaes.models.unit_models"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.models.unit_models.pressure_changer import ThermodynamicAssumption\n",
- "from idaes.core.util.model_statistics import degrees_of_freedom\n",
- "\n",
- "# Import idaes logger to set output levels\n",
- "import idaes.logger as idaeslog\n",
- "from idaes.core.solvers import get_solver\n",
- "from idaes.core.util.exceptions import InitializationError"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Importing required thermo and reaction package\n",
- "\n",
- "The final set of imports are to import the thermo and reaction package for the HDA process. We have created a custom thermo package that assumes Ideal Gas with support for VLE. \n",
- "\n",
- "The reaction package here is very simple as we will be using only a StochiometricReactor and the reaction package consists of the stochiometric coefficients for the reaction and the parameter for the heat of reaction. \n",
- "\n",
- "Let us import the following modules and they are in the same directory as this jupyter notebook:\n",
- "
\n",
- " - hda_ideal_VLE as thermo_props
\n",
- " - hda_reaction as reaction_props
\n",
- "
\n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes_examples.mod.hda import hda_ideal_VLE as thermo_props\n",
- "from idaes_examples.mod.hda import hda_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 as we did in module 1. "
- ]
- },
- {
- "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 Module 1, where we only had a thermo property package, for this flowsheet we will also need to add a reaction property package. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.thermo_params = thermo_props.HDAParameterBlock()\n",
- "m.fs.reaction_params = reaction_props.HDAReactionParameterBlock(\n",
- " property_package=m.fs.thermo_params\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 the Mixer (assigned a name M101) and a Heater (assigned a name H101). 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 (https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/unit_models/index.html). For example, the Mixer unit model here is given a `list` consisting of names to the three inlets. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.M101 = Mixer(\n",
- " property_package=m.fs.thermo_params,\n",
- " inlet_list=[\"toluene_feed\", \"hydrogen_feed\", \"vapor_recycle\"],\n",
- ")\n",
- "\n",
- "m.fs.H101 = Heater(\n",
- " property_package=m.fs.thermo_params,\n",
- " has_pressure_change=False,\n",
- " has_phase_equilibrium=True,\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "
Inline Exercise:\n",
- "Let us now add the StoichiometricReactor(assign the name R101) and pass the following arguments:\n",
- "
\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",
- "
\n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Add reactor with the specifications above"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us now add the Flash(assign the name F101) and pass the following arguments:\n",
- "
\n",
- " - \"property_package\": m.fs.thermo_params
\n",
- " - \"has_heat_transfer\": True
\n",
- " - \"has_pressure_change\": False
\n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.F101 = Flash(\n",
- " property_package=m.fs.thermo_params,\n",
- " has_heat_transfer=True,\n",
- " has_pressure_change=True,\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us now add the Splitter(S101), PressureChanger(C101) and the second Flash(F102). "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.S101 = Splitter(\n",
- " property_package=m.fs.thermo_params,\n",
- " ideal_separation=False,\n",
- " outlet_list=[\"purge\", \"recycle\"],\n",
- ")\n",
- "\n",
- "\n",
- "m.fs.C101 = PressureChanger(\n",
- " property_package=m.fs.thermo_params,\n",
- " compressor=True,\n",
- " thermodynamic_assumption=ThermodynamicAssumption.isothermal,\n",
- ")\n",
- "\n",
- "m.fs.F102 = Flash(\n",
- " property_package=m.fs.thermo_params,\n",
- " has_heat_transfer=True,\n",
- " has_pressure_change=True,\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(M101) to the inlet of the heater(H101). "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- " \n",
- "\n",
- "
\n",
- "Inline Exercise:\n",
- "Now, connect the H101 outlet to the R101 inlet using the cell above as a guide. \n",
- "
\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Connect the H101 outlet to R101 inlet"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We will now be connecting the rest of the flowsheet as shown below. Notice how the outlet names are different for the flash tanks F101 and F102 as they have a vapor and a liquid outlet. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.F101.inlet)\n",
- "m.fs.s06 = Arc(source=m.fs.F101.vap_outlet, destination=m.fs.S101.inlet)\n",
- "m.fs.s08 = Arc(source=m.fs.S101.recycle, destination=m.fs.C101.inlet)\n",
- "m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.vapor_recycle)\n",
- "m.fs.s10 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We have now connected the unit model block using the arcs. However, each of these arcs link to ports on the two unit models that are connected. In this case, the ports consist of the state variables that need to be linked between the unit models. Pyomo provides a convenient method to write these equality constraints for us between two ports and this is done as follows:"
- ]
- },
- {
- "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 purity and 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: https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html\n",
- "\n",
- "For this flowsheet, we are interested in computing the purity of the product Benzene stream (i.e. the mole fraction) and the operating cost which is a sum of the cooling and heating cost. "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us first add an Expression to compute the mole fraction of benzene in the `vap_outlet` of F102 which is our product stream. Please note that the var flow_mol_phase_comp has the index - [time, phase, component]. As this is a steady-state flowsheet, the time index by default is 0. The valid phases are [\"Liq\", \"Vap\"]. Similarly the valid component list is [\"benzene\", \"toluene\", \"hydrogen\", \"methane\"]."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.purity = Expression(\n",
- " expr=m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- " / (\n",
- " m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- " + m.fs.F102.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
- " )\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now, let us add an expression to compute the cooling cost assuming a cost of 0.212E-4 $/kW. Note that cooling utility is required for the reactor (R101) and the first flash (F101). "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.cooling_cost = Expression(\n",
- " expr=0.212e-7 * (-m.fs.F101.heat_duty[0]) + 0.212e-7 * (-m.fs.R101.heat_duty[0])\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- "Now, let us add an expression to compute the heating cost assuming the utility cost as follows:\n",
- "
\n",
- " - 2.2E-4 dollars/kW for H101
\n",
- " - 1.9E-4 dollars/kW for F102
\n",
- "
\n",
- "Note that the heat duty is in units of watt (J/s). "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.heating_cost = Expression(\n",
- " expr=2.2e-7 * m.fs.H101.heat_duty[0] + 1.9e-7 * m.fs.F102.heat_duty[0]\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us now add an expression to compute the total operating cost per year which is basically the sum of the cooling and heating cost we defined above. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.operating_cost = Expression(\n",
- " expr=(3600 * 24 * 365 * (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. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(degrees_of_freedom(m))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We will now be fixing the toluene feed stream to the conditions shown in the flowsheet above. Please note that though this is a pure toluene feed, the remaining components are still assigned a very small non-zero value to help with convergence and initializing. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"toluene\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"hydrogen\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Vap\", \"methane\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"toluene\"].fix(0.30)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"hydrogen\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.flow_mol_phase_comp[0, \"Liq\", \"methane\"].fix(1e-5)\n",
- "m.fs.M101.toluene_feed.temperature.fix(303.2)\n",
- "m.fs.M101.toluene_feed.pressure.fix(350000)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- "Similarly, let us fix the hydrogen feed to the following conditions in the next cell:\n",
- "
\n",
- " - FH2 = 0.30 mol/s
\n",
- " - FCH4 = 0.02 mol/s
\n",
- " - Remaining components = 1e-5 mol/s
\n",
- " - T = 303.2 K
\n",
- " - P = 350000 Pa
\n",
- "
\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"toluene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"hydrogen\"].fix(0.30)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Vap\", \"methane\"].fix(0.02)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"benzene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"toluene\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"hydrogen\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, \"Liq\", \"methane\"].fix(1e-5)\n",
- "m.fs.M101.hydrogen_feed.temperature.fix(303.2)\n",
- "m.fs.M101.hydrogen_feed.pressure.fix(350000)"
- ]
- },
- {
- "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 set set the H101 outlet temperature to 600 K. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.H101.outlet.temperature.fix(600)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "For the StoichiometricReactor, we have to define the conversion in terms of toluene. This requires us to create a new variable for specifying the conversion and adding a Constraint that defines the conversion with respect to toluene. The second degree of freedom for the reactor is to define the heat duty. In this case, let us assume the reactor to be adiabatic i.e. Q = 0. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.R101.conversion = Var(initialize=0.75, bounds=(0, 1))\n",
- "\n",
- "m.fs.R101.conv_constraint = Constraint(\n",
- " expr=m.fs.R101.conversion * m.fs.R101.inlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
- " == (\n",
- " m.fs.R101.inlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
- " - m.fs.R101.outlet.flow_mol_phase_comp[0, \"Vap\", \"toluene\"]\n",
- " )\n",
- ")\n",
- "\n",
- "m.fs.R101.conversion.fix(0.75)\n",
- "m.fs.R101.heat_duty.fix(0)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "The Flash conditions for F101 can be set as follows. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.F101.vap_outlet.temperature.fix(325.0)\n",
- "m.fs.F101.deltaP.fix(0)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "
Inline Exercise:\n",
- "Set the conditions for Flash F102 to the following conditions:\n",
- "
\n",
- " - T = 375 K
\n",
- " - deltaP = -200000
\n",
- "
\n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Set conditions for Flash F102"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us fix the purge split fraction to 20% and the outlet pressure of the compressor is set to 350000 Pa. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.S101.split_fraction[0, \"purge\"].fix(0.2)\n",
- "m.fs.C101.outlet.pressure.fix(350000)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "We have now defined all the feed conditions and the inputs required for the unit models. The system should now have 0 degrees of freedom i.e. should be a square problem. Please check that the degrees of freedom is 0. \n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: print the degrees of freedom"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Initialization\n",
- "\n",
- "\n",
- "This section will demonstrate how to use the built-in sequential decomposition tool to initialize our flowsheet.\n",
- "\n",
- " \n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us first create an object for the SequentialDecomposition and specify our options for this. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "seq = SequentialDecomposition()\n",
- "seq.options.select_tear_method = \"heuristic\"\n",
- "seq.options.tear_method = \"Wegstein\"\n",
- "seq.options.iterLim = 3\n",
- "\n",
- "# Using the SD tool\n",
- "G = seq.create_graph(m)\n",
- "heuristic_tear_set = seq.tear_set_arcs(G, method=\"heuristic\")\n",
- "order = seq.calculation_order(G)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Which is the tear stream? Display tear set and order"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "for o in heuristic_tear_set:\n",
- " print(o.name)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "What sequence did the SD tool determine to solve this flowsheet with the least number of tears? "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "scrolled": true
- },
- "outputs": [],
- "source": [
- "for o in order:\n",
- " print(o[0].name)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- " \n",
- "\n",
- " \n",
- "\n",
- "\n",
- "The SequentialDecomposition tool has determined that the tear stream is the mixer outlet. We will need to provide a reasonable guess for this."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "tear_guesses = {\n",
- " \"flow_mol_phase_comp\": {\n",
- " (0, \"Vap\", \"benzene\"): 1e-5,\n",
- " (0, \"Vap\", \"toluene\"): 1e-5,\n",
- " (0, \"Vap\", \"hydrogen\"): 0.30,\n",
- " (0, \"Vap\", \"methane\"): 0.02,\n",
- " (0, \"Liq\", \"benzene\"): 1e-5,\n",
- " (0, \"Liq\", \"toluene\"): 0.30,\n",
- " (0, \"Liq\", \"hydrogen\"): 1e-5,\n",
- " (0, \"Liq\", \"methane\"): 1e-5,\n",
- " },\n",
- " \"temperature\": {0: 303},\n",
- " \"pressure\": {0: 350000},\n",
- "}\n",
- "\n",
- "# Pass the tear_guess to the SD tool\n",
- "seq.set_guesses_for(m.fs.H101.inlet, tear_guesses)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Next, we need to tell the tool how to initialize a particular unit. We will be writing a python function which takes in a \"unit\" and calls the initialize method on that unit. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "def function(unit):\n",
- " try:\n",
- " initializer = unit.default_initializer()\n",
- " initializer.initialize(unit, output_level=idaeslog.INFO)\n",
- " except InitializationError:\n",
- " solver = get_solver()\n",
- " solver.solve(unit)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We are now ready to initialize our flowsheet in a sequential mode. Note that we specifically set the iteration limit to be 5 as we are trying to use this tool only to get a good set of initial values such that IPOPT can then take over and solve this flowsheet for us. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "seq.run(m, function)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "We have now initialized the flowsheet. Let us run the flowsheet in a simulation mode to look at the results. To do this, complete the last line of code where we pass the model to the solver. You will need to type the following:\n",
- " \n",
- "results = solver.solve(m, tee=True)\n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Create the solver object\n",
- "\n",
- "\n",
- "# Solve the model"
- ]
- },
- {
- "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(\"operating cost = $\", value(m.fs.operating_cost))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "For this operating cost, what is the amount of benzene we are able to produce and what purity we are able to achieve? "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.F102.report()\n",
- "\n",
- "print()\n",
- "print(\"benzene purity = \", value(m.fs.purity))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Next, let's look at how much benzene we are losing with the light gases out of F101. IDAES has tools for creating stream tables based on the `Arcs` and/or `Ports` in a flowsheet. Let us create and print a simple stream table showing the stream leaving the reactor and the vapor stream from F101.\n",
- "\n",
- "
\n",
- "Inline Exercise:\n",
- "How much benzene are we losing in the F101 vapor outlet stream?\n",
- "
\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.core.util.tables import (\n",
- " create_stream_table_dataframe,\n",
- " stream_table_dataframe_to_string,\n",
- ")\n",
- "\n",
- "st = create_stream_table_dataframe({\"Reactor\": m.fs.s05, \"Light Gases\": m.fs.s06})\n",
- "print(stream_table_dataframe_to_string(st))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "You can query additional variables here if you like. \n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Optimization\n",
- "\n",
- "\n",
- "We saw from the results above that the total operating cost for the base case was $419,122 per year. We are producing 0.142 mol/s of benzene at a purity of 82\\%. However, we are losing around 42\\% of benzene in F101 vapor outlet stream. \n",
- "\n",
- "Let us try to minimize this cost such that:\n",
- "- we are producing at least 0.15 mol/s of benzene in F102 vapor outlet i.e. our product stream\n",
- "- purity of benzene i.e. the mole fraction of benzene in F102 vapor outlet is at least 80%\n",
- "- restricting the benzene loss in F101 vapor outlet to less than 20%\n",
- "\n",
- "For this problem, our decision variables are as follows:\n",
- "- H101 outlet temperature\n",
- "- R101 cooling duty provided\n",
- "- F101 outlet temperature\n",
- "- F102 outlet temperature\n",
- "- F102 deltaP in the flash tank\n"
- ]
- },
- {
- "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 unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.H101.outlet.temperature.unfix()\n",
- "m.fs.R101.heat_duty.unfix()\n",
- "m.fs.F101.vap_outlet.temperature.unfix()\n",
- "m.fs.F102.vap_outlet.temperature.unfix()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "Let us now unfix the remaining variable which is F102 pressure drop (F102.deltaP) \n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
\n",
- "\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Unfix deltaP for F102"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Next, we need to set bounds on these decision variables to values shown below:\n",
- "\n",
- " - H101 outlet temperature [500, 600] K\n",
- " - R101 outlet temperature [600, 800] K\n",
- " - F101 outlet temperature [298, 450] K\n",
- " - F102 outlet temperature [298, 450] K\n",
- " - F102 outlet pressure [105000, 110000] Pa\n",
- "\n",
- "Let us first set the variable bound for the H101 outlet temperature as shown below:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.H101.outlet.temperature[0].setlb(500)\n",
- "m.fs.H101.outlet.temperature[0].setub(600)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "Now, set the variable bound for the R101 outlet temperature.\n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Set the bounds for reactor outlet temperature"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us fix the bounds for the rest of the decision variables. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.F101.vap_outlet.temperature[0].setlb(298.0)\n",
- "m.fs.F101.vap_outlet.temperature[0].setub(450.0)\n",
- "m.fs.F102.vap_outlet.temperature[0].setlb(298.0)\n",
- "m.fs.F102.vap_outlet.temperature[0].setub(450.0)\n",
- "m.fs.F102.vap_outlet.pressure[0].setlb(105000)\n",
- "m.fs.F102.vap_outlet.pressure[0].setub(110000)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Now, the only things left to define are our constraints on overhead loss in F101, product flow rate and purity in F102. Let us first look at defining a constraint for the overhead loss in F101 where we are restricting the benzene leaving the vapor stream to less than 20 \\% of the benzene available in the reactor outlet. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.overhead_loss = Constraint(\n",
- " expr=m.fs.F101.vap_outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- " <= 0.20 * m.fs.R101.outlet.flow_mol_phase_comp[0, \"Vap\", \"benzene\"]\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "Now, add the constraint such that we are producing at least 0.15 mol/s of benzene in the product stream which is the vapor outlet of F102. Let us name this constraint as m.fs.product_flow. \n",
- "\n",
- "Use Shift+Enter to run the cell once you have typed in your code. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Add minimum product flow constraint"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let us add the final constraint on product purity or the mole fraction of benzene in the product stream such that it is at least greater than 80%. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.product_purity = Constraint(expr=m.fs.purity >= 0.80)"
- ]
- },
- {
- "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": "markdown",
- "metadata": {},
- "source": [
- "## Optimization Results\n",
- "\n",
- "Display the results and product specifications"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(\"operating cost = $\", value(m.fs.operating_cost))\n",
- "\n",
- "print()\n",
- "print(\"Product flow rate and purity in F102\")\n",
- "\n",
- "m.fs.F102.report()\n",
- "\n",
- "print()\n",
- "print(\"benzene purity = \", value(m.fs.purity))\n",
- "\n",
- "print()\n",
- "print(\"Overhead loss in F101\")\n",
- "m.fs.F101.report()"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Display optimal values for the decision variables"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(\"Optimal Values\")\n",
- "print()\n",
- "\n",
- "print(\"H101 outlet temperature = \", value(m.fs.H101.outlet.temperature[0]), \"K\")\n",
- "\n",
- "print()\n",
- "print(\"R101 outlet temperature = \", value(m.fs.R101.outlet.temperature[0]), \"K\")\n",
- "\n",
- "print()\n",
- "print(\"F101 outlet temperature = \", value(m.fs.F101.vap_outlet.temperature[0]), \"K\")\n",
- "\n",
- "print()\n",
- "print(\"F102 outlet temperature = \", value(m.fs.F102.vap_outlet.temperature[0]), \"K\")\n",
- "print(\"F102 outlet pressure = \", value(m.fs.F102.vap_outlet.pressure[0]), \"Pa\")"
- ]
- },
- {
- "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.8.12"
- }
- },
- "nbformat": 4,
- "nbformat_minor": 3
+ "cells": [
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "header",
+ "hide-cell"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:27.028013200Z",
+ "start_time": "2026-01-13T00:12:27.015275700Z"
+ }
+ },
+ "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",
+ "###############################################################################"
+ ],
+ "outputs": [],
+ "execution_count": 1
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "# HDA Flowsheet Simulation and Optimization\n",
+ "\n",
+ "Author: Jaffer Ghouse
\n",
+ "Maintainer: Tanner Polley
\n",
+ "Updated: 2026-1-12\n",
+ "\n",
+ "## Learning outcomes\n",
+ "\n",
+ "\n",
+ "- Construct a steady-state flowsheet using the IDAES unit model library\n",
+ "- Connecting unit models in a flowsheet using Arcs\n",
+ "- Using the SequentialDecomposition tool to initialize a flowsheet with recycle\n",
+ "- Formulate and solve an optimization problem\n",
+ " - Defining an objective function\n",
+ " - Setting variable bounds\n",
+ " - Adding additional constraints\n",
+ "\n",
+ "\n",
+ "The general workflow of setting up an IDAES flowsheet is the following:\n",
+ "\n",
+ " 1 Importing Modules
\n",
+ " 2 Building a Model
\n",
+ " 3 Scaling the Model
\n",
+ " 4 Specifying the Model
\n",
+ " 5 Initializing the Model
\n",
+ " 6 Solving the Model
\n",
+ " 7 Analyzing and Visualizing the Results
\n",
+ " 8 Optimizing the Model
\n",
+ "\n",
+ "We will complete each of these steps as well as demonstrate analyses on this model through some examples and exercises.\n",
+ "\n",
+ "\n",
+ "## Problem Statement\n",
+ "\n",
+ "Hydrodealkylation is a chemical reaction that often involves reacting\n",
+ "an aromatic hydrocarbon in the presence of hydrogen gas to form a\n",
+ "simpler aromatic hydrocarbon devoid of functional groups. In this\n",
+ "example, toluene will be reacted with hydrogen gas at high temperatures\n",
+ " to form benzene via the following reaction:\n",
+ "\n",
+ "**C
6H
5CH
3 + H
2 \u2192 C
6H
6 + CH
4**\n",
+ "\n",
+ "\n",
+ "This reaction is often accompanied by an equilibrium side reaction\n",
+ "which forms diphenyl, which we will neglect for this example.\n",
+ "\n",
+ "This example is based on the 1967 AIChE Student Contest problem as\n",
+ "present by Douglas, J.M., Chemical Design of Chemical Processes, 1988,\n",
+ "McGraw-Hill.\n",
+ "\n",
+ "The flowsheet that we will be using for this module is shown below with the stream conditions. We will be processing toluene and hydrogen to produce at least 370 TPY of benzene. As shown in the flowsheet, there are two flash tanks, F101 to separate out the non-condensibles and F102 to further separate the benzene-toluene mixture to improve the benzene purity. Note that typically a distillation column is required to obtain high purity benzene but that is beyond the scope of this workshop. The non-condensibles separated out in F101 will be partially recycled back to M101 and the rest will be either purged or combusted for power generation.We will assume ideal gas for this flowsheet. The properties required for this module are available in the same directory:\n",
+ "\n",
+ "- hda_ideal_VLE.py\n",
+ "- hda_reaction.py\n",
+ "\n",
+ "The state variables chosen for the property package are **flows of component by phase, temperature and pressure**. The components considered are: **toluene, hydrogen, benzene and methane**. Therefore, every stream has 8 flow variables, 1 temperature and 1 pressure variable. \n",
+ "\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 1 Importing Modules\n",
+ "### 1.1 Importing required Pyomo and IDAES components\n",
+ "\n",
+ "\n",
+ "To construct a flowsheet, we will need several components from the Pyomo and IDAES package. 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",
+ "- SolverFactory (to solve the problem)\n",
+ "- TransformationFactory (to apply certain transformations)\n",
+ "- Arc (to connect two unit models)\n",
+ "- SequentialDecomposition (to initialize the flowsheet in a sequential mode)\n",
+ "\n",
+ "For further details on these components, please refer to the Pyomo documentation: https://Pyomo.readthedocs.io/en/stable/\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:27.614407500Z",
+ "start_time": "2026-01-13T00:12:27.029098600Z"
+ }
+ },
+ "source": [
+ "from pyomo.environ import (\n",
+ " Constraint,\n",
+ " Var,\n",
+ " ConcreteModel,\n",
+ " Expression,\n",
+ " Objective,\n",
+ " TransformationFactory,\n",
+ " value,\n",
+ ")\n",
+ "from pyomo.network import Arc"
+ ],
+ "outputs": [],
+ "execution_count": 2
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "From IDAES, we will be needing the FlowsheetBlock and the following unit models:\n",
+ "- Feed\n",
+ "- Mixer\n",
+ "- Heater\n",
+ "- StoichiometricReactor\n",
+ "-
**Flash**\n",
+ "- Separator (splitter) \n",
+ "- PressureChanger\n",
+ "- Product"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.427113300Z",
+ "start_time": "2026-01-13T00:12:27.699286500Z"
+ }
+ },
+ "source": "from idaes.core import FlowsheetBlock",
+ "outputs": [],
+ "execution_count": 3
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.495404700Z",
+ "start_time": "2026-01-13T00:12:29.432738900Z"
+ }
+ },
+ "source": [
+ "from idaes.models.unit_models import (\n",
+ " PressureChanger,\n",
+ " Mixer,\n",
+ " Separator as Splitter,\n",
+ " Heater,\n",
+ " StoichiometricReactor,\n",
+ " Feed,\n",
+ " Product,\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 4
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "
\n",
+ "Inline Exercise:\n",
+ "Now, import the remaining unit models highlighted in blue above and run the cell using `Shift+Enter` after typing in the code. \n",
+ "
\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.504917Z",
+ "start_time": "2026-01-13T00:12:29.497935600Z"
+ }
+ },
+ "source": [
+ "# Todo: import flash model from idaes.models.unit_models"
+ ],
+ "outputs": [],
+ "execution_count": 5
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.539485800Z",
+ "start_time": "2026-01-13T00:12:29.531348400Z"
+ }
+ },
+ "source": [
+ "from idaes.models.unit_models.pressure_changer import ThermodynamicAssumption\n",
+ "from idaes.core.util.model_statistics import degrees_of_freedom\n",
+ "\n",
+ "# Import idaes logger to set output levels\n",
+ "from idaes.core.solvers import get_solver"
+ ],
+ "outputs": [],
+ "execution_count": 7
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 1.2 Importing required thermo and reaction package\n",
+ "\n",
+ "The final set of imports are to import the thermo and reaction package for the HDA process. We have created a custom thermo package that assumes Ideal Gas with support for VLE. \n",
+ "\n",
+ "The reaction package here is very simple as we will be using only a StochiometricReactor and the reaction package consists of the stochiometric coefficients for the reaction and the parameter for the heat of reaction. \n",
+ "\n",
+ "Let us import the following modules and they are in the same directory as this jupyter notebook:\n",
+ "
\n",
+ " - hda_ideal_VLE as thermo_props
\n",
+ " - hda_reaction as reaction_props
\n",
+ "
\n",
+ "
"
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.627980300Z",
+ "start_time": "2026-01-13T00:12:29.540486900Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "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_examples.mod.hda.hda_ideal_VLE_modular import thermo_config\n",
+ "from idaes_examples.mod.hda.hda_reaction_modular import reaction_config"
+ ],
+ "outputs": [],
+ "execution_count": 8
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 2 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",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.638672500Z",
+ "start_time": "2026-01-13T00:12:29.630018Z"
+ }
+ },
+ "source": [
+ "m = ConcreteModel()\n",
+ "m.fs = FlowsheetBlock(dynamic=False)"
+ ],
+ "outputs": [],
+ "execution_count": 9
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We now need to add the property packages to the flowsheet. Unlike Module 1, where we only had a thermo property package, for this flowsheet we will also need to add a reaction property package. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.669834600Z",
+ "start_time": "2026-01-13T00:12:29.638672500Z"
+ }
+ },
+ "source": [
+ "m.fs.thermo_params = GenericParameterBlock(**thermo_config)\n",
+ "m.fs.reaction_params = GenericReactionParameterBlock(\n",
+ " property_package=m.fs.thermo_params, **reaction_config\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 10
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 2.1 Adding Unit Models\n",
+ "\n",
+ "Let us start adding the unit models we have imported to the flowsheet. Here, we are adding the Feed (assigned a name `I101` for Inlet), `Mixer` (assigned a name `M101`) and a `Heater` (assigned a name `H101`). 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 (https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/unit_models/index.html). For example, the `Mixer` unit model here must be specified the number of inlets that it will take in and the `Heater` can have specific settings enabled such as `has_pressure_change` or `has_phase_equilibrium`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.718708300Z",
+ "start_time": "2026-01-13T00:12:29.672813900Z"
+ }
+ },
+ "source": [
+ "m.fs.I101 = Feed(property_package=m.fs.thermo_params)\n",
+ "m.fs.I102 = Feed(property_package=m.fs.thermo_params)\n",
+ "\n",
+ "m.fs.M101 = Mixer(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " num_inlets=3,\n",
+ ")\n",
+ "\n",
+ "m.fs.H101 = Heater(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " has_pressure_change=False,\n",
+ " has_phase_equilibrium=True,\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 11
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
+ "source": [
+ "
\n",
+ "
Inline Exercise:\n",
+ "Let us now add the StoichiometricReactor(assign the name R101) and pass the following arguments:\n",
+ "
\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",
+ "
\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.737260500Z",
+ "start_time": "2026-01-13T00:12:29.719754300Z"
+ }
+ },
+ "source": [
+ "# Todo: Add reactor with the specifications above"
+ ],
+ "outputs": [],
+ "execution_count": 12
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us now add the Flash(assign the name F101) and pass the following arguments:\n",
+ "
\n",
+ " - \"property_package\": m.fs.thermo_params
\n",
+ " - \"has_heat_transfer\": True
\n",
+ " - \"has_pressure_change\": False
\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.785139300Z",
+ "start_time": "2026-01-13T00:12:29.762955300Z"
+ }
+ },
+ "source": [
+ "m.fs.F101 = Flash(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " has_heat_transfer=True,\n",
+ " has_pressure_change=True,\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 14
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us now add the Splitter(S101) with specific names for its output (purge and recycle), PressureChanger(C101) and the second Flash(F102)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.827217600Z",
+ "start_time": "2026-01-13T00:12:29.787651900Z"
+ }
+ },
+ "source": [
+ "m.fs.S101 = Splitter(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " ideal_separation=False,\n",
+ " outlet_list=[\"purge\", \"recycle\"],\n",
+ ")\n",
+ "\n",
+ "\n",
+ "m.fs.C101 = PressureChanger(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " compressor=True,\n",
+ " thermodynamic_assumption=ThermodynamicAssumption.isothermal,\n",
+ ")\n",
+ "\n",
+ "m.fs.F102 = Flash(\n",
+ " property_package=m.fs.thermo_params,\n",
+ " has_heat_transfer=True,\n",
+ " has_pressure_change=True,\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 15
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Last, we will add the three Product blocks (P101, P102, P103). We use `Feed` blocks and `Product` blocks for convenience with reporting stream summaries and consistency"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.844845200Z",
+ "start_time": "2026-01-13T00:12:29.829763300Z"
+ }
+ },
+ "source": [
+ "m.fs.P101 = Product(property_package=m.fs.thermo_params)\n",
+ "m.fs.P102 = Product(property_package=m.fs.thermo_params)\n",
+ "m.fs.P103 = Product(property_package=m.fs.thermo_params)"
+ ],
+ "outputs": [],
+ "execution_count": 16
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 2.2 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 inlets (I101, I102) to the inlet of the mixer (M101) and outlet of the mixer to the inlet of the heater(H101)."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ ""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.855598100Z",
+ "start_time": "2026-01-13T00:12:29.847349400Z"
+ }
+ },
+ "source": [
+ "m.fs.s01 = Arc(source=m.fs.I101.outlet, destination=m.fs.M101.inlet_1)\n",
+ "m.fs.s02 = Arc(source=m.fs.I102.outlet, destination=m.fs.M101.inlet_2)\n",
+ "m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet)"
+ ],
+ "outputs": [],
+ "execution_count": 17
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
+ "source": [
+ "
\n",
+ "Inline Exercise:\n",
+ "Now, connect the H101 outlet to the R101 inlet using the cell above as a guide.\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.865134200Z",
+ "start_time": "2026-01-13T00:12:29.857100Z"
+ }
+ },
+ "source": [
+ "# Todo: Connect the H101 outlet to R101 inlet"
+ ],
+ "outputs": [],
+ "execution_count": 18
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We will now be connecting the rest of the flowsheet as shown below. Notice how the outlet names are different for the flash tanks F101 and F102 as they have a vapor and a liquid outlet. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.882527100Z",
+ "start_time": "2026-01-13T00:12:29.874065200Z"
+ }
+ },
+ "source": [
+ "m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.F101.inlet)\n",
+ "m.fs.s06 = Arc(source=m.fs.F101.vap_outlet, destination=m.fs.S101.inlet)\n",
+ "m.fs.s07 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet)\n",
+ "m.fs.s08 = Arc(source=m.fs.S101.recycle, destination=m.fs.C101.inlet)\n",
+ "m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.inlet_3)"
+ ],
+ "outputs": [],
+ "execution_count": 20
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Last we will connect the outlet streams to the inlets of the Product blocks (P101, P102, P103)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.891441800Z",
+ "start_time": "2026-01-13T00:12:29.882527100Z"
+ }
+ },
+ "source": [
+ "m.fs.s10 = Arc(source=m.fs.F102.vap_outlet, destination=m.fs.P101.inlet)\n",
+ "m.fs.s11 = Arc(source=m.fs.F102.liq_outlet, destination=m.fs.P102.inlet)\n",
+ "m.fs.s12 = Arc(source=m.fs.S101.purge, destination=m.fs.P103.inlet)"
+ ],
+ "outputs": [],
+ "execution_count": 21
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We have now connected the unit model block using the arcs. However, each of these arcs link to ports on the two unit models that are connected. In this case, the ports consist of the state variables that need to be linked between the unit models. Pyomo provides a convenient method to write these equality constraints for us between two ports and this is done as follows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.916831200Z",
+ "start_time": "2026-01-13T00:12:29.891441800Z"
+ }
+ },
+ "source": [
+ "TransformationFactory(\"network.expand_arcs\").apply_to(m)"
+ ],
+ "outputs": [],
+ "execution_count": 22
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 2.3 Adding expressions to compute purity and 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: https://pyomo.readthedocs.io/en/stable/explanation/modeling/network.html.\n",
+ "\n",
+ "For this flowsheet, we are interested in computing the purity of the product Benzene stream (i.e. the mole fraction) and the operating cost which is a sum of the cooling and heating cost. "
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us first add an Expression to compute the mole fraction of benzene in the `vap_outlet` of F102 which is our product stream. Please note that the var flow_mol_phase_comp has the index - [time, phase, component]. As this is a steady-state flowsheet, the time index by default is 0. The valid phases are [\"Liq\", \"Vap\"]. Similarly the valid component list is [\"benzene\", \"toluene\", \"hydrogen\", \"methane\"]."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.926950800Z",
+ "start_time": "2026-01-13T00:12:29.918895100Z"
+ }
+ },
+ "source": [
+ "m.fs.purity = Expression(\n",
+ " expr=m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"benzene\"\n",
+ " ]\n",
+ " / (\n",
+ " m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\"Vap\", \"benzene\"]\n",
+ " + m.fs.F102.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"toluene\"\n",
+ " ]\n",
+ " )\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 23
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, let us add an expression to compute the cooling cost assuming a cost of 0.212E-4 $/kW. Note that cooling utility is required for the reactor (R101) and the first flash (F101). "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.937452200Z",
+ "start_time": "2026-01-13T00:12:29.928143Z"
+ }
+ },
+ "source": [
+ "m.fs.cooling_cost = Expression(\n",
+ " expr=0.212e-7 * (-m.fs.F101.heat_duty[0]) + 0.212e-7 * (-m.fs.R101.heat_duty[0])\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 24
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "Now, let us add an expression to compute the heating cost assuming the utility cost as follows:\n",
+ "
\n",
+ " - 2.2E-4 dollars/kW for H101
\n",
+ " - 1.9E-4 dollars/kW for F102
\n",
+ "
\n",
+ "Note that the heat duty is in units of watt (J/s). "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.945656300Z",
+ "start_time": "2026-01-13T00:12:29.938460700Z"
+ }
+ },
+ "source": [
+ "m.fs.heating_cost = Expression(\n",
+ " expr=2.2e-7 * m.fs.H101.heat_duty[0] + 1.9e-7 * m.fs.F102.heat_duty[0]\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 25
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us now add an expression to compute the total operating cost per year which is basically the sum of the cooling and heating cost we defined above. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.954918200Z",
+ "start_time": "2026-01-13T00:12:29.947160400Z"
+ }
+ },
+ "source": [
+ "m.fs.operating_cost = Expression(\n",
+ " expr=(3600 * 24 * 365 * (m.fs.heating_cost + m.fs.cooling_cost))\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 26
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 4 Specifying the Model\n",
+ "### 4.1 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. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:29.987120200Z",
+ "start_time": "2026-01-13T00:12:29.954918200Z"
+ }
+ },
+ "source": [
+ "print(degrees_of_freedom(m))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "29\n"
+ ]
+ }
+ ],
+ "execution_count": 27
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We will now be fixing the toluene feed (`I101`) stream to the conditions shown in the flowsheet above. Please note\n",
+ "that though this is a pure toluene feed, the remaining components are still assigned a very small non-zero value to\n",
+ "help with convergence and initializing. We will be importing a function that will specify the inlet conditions for\n",
+ "this example."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.037749600Z",
+ "start_time": "2026-01-13T00:12:30.025896900Z"
+ }
+ },
+ "source": [
+ "from idaes_examples.mod.hda.hda_flowsheet_extras import fix_inlet_states\n",
+ "\n",
+ "tear_guesses = fix_inlet_states(m)"
+ ],
+ "outputs": [],
+ "execution_count": 29
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 4.2 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 set set the H101 outlet temperature to 600 K. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.047997400Z",
+ "start_time": "2026-01-13T00:12:30.039779600Z"
+ }
+ },
+ "source": [
+ "m.fs.H101.outlet.temperature.fix(600)"
+ ],
+ "outputs": [],
+ "execution_count": 30
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For the StoichiometricReactor, we have to define the conversion in terms of toluene. This requires us to create a new variable for specifying the conversion and adding a Constraint that defines the conversion with respect to toluene. The second degree of freedom for the reactor is to define the heat duty. In this case, let us assume the reactor to be adiabatic i.e. Q = 0. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.057083400Z",
+ "start_time": "2026-01-13T00:12:30.047997400Z"
+ }
+ },
+ "source": [
+ "m.fs.R101.conversion = Var(initialize=0.75, bounds=(0, 1))\n",
+ "\n",
+ "m.fs.R101.conv_constraint = Constraint(\n",
+ " expr=m.fs.R101.conversion\n",
+ " * (m.fs.R101.control_volume.properties_in[0].flow_mol_phase_comp[\"Vap\", \"toluene\"])\n",
+ " == (\n",
+ " m.fs.R101.control_volume.properties_in[0].flow_mol_phase_comp[\"Vap\", \"toluene\"]\n",
+ " - m.fs.R101.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"toluene\"\n",
+ " ]\n",
+ " )\n",
+ ")\n",
+ "\n",
+ "m.fs.R101.conversion.fix(0.75)\n",
+ "m.fs.R101.heat_duty.fix(0)"
+ ],
+ "outputs": [],
+ "execution_count": 31
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The Flash conditions for F101 can be set as follows. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.064658700Z",
+ "start_time": "2026-01-13T00:12:30.057083400Z"
+ }
+ },
+ "source": [
+ "m.fs.F101.vap_outlet.temperature.fix(325.0)\n",
+ "m.fs.F101.deltaP.fix(0)"
+ ],
+ "outputs": [],
+ "execution_count": 32
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
+ "source": [
+ "
\n",
+ "
Inline Exercise:\n",
+ "Set the conditions for Flash F102 to the following conditions:\n",
+ "
\n",
+ " - T = 375 K
\n",
+ " - deltaP = -200000
\n",
+ "
\n",
+ "\n",
+ "Use Shift+Enter to run the cell once you have typed in your code. \n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.072457700Z",
+ "start_time": "2026-01-13T00:12:30.064658700Z"
+ }
+ },
+ "source": [
+ "# Todo: Set conditions for Flash F102"
+ ],
+ "outputs": [],
+ "execution_count": 33
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us fix the purge split fraction to 20% and the outlet pressure of the compressor is set to 350000 Pa. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.088622800Z",
+ "start_time": "2026-01-13T00:12:30.079966500Z"
+ }
+ },
+ "source": [
+ "m.fs.S101.split_fraction[0, \"purge\"].fix(0.2)\n",
+ "m.fs.C101.outlet.pressure.fix(350000)"
+ ],
+ "outputs": [],
+ "execution_count": 35
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
+ "source": [
+ "
\n",
+ "Inline Exercise:\n",
+ "We have now defined all the feed conditions and the inputs required for the unit models. The system should now have 0 degrees of freedom i.e. should be a square problem. Please check that the degrees of freedom is 0. \n",
+ "\n",
+ "Use Shift+Enter to run the cell once you have typed in your code. \n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.096269500Z",
+ "start_time": "2026-01-13T00:12:30.088622800Z"
+ }
+ },
+ "source": [
+ "# Todo: print the degrees of freedom"
+ ],
+ "outputs": [],
+ "execution_count": 36
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 5 Initializing the Model\n",
+ "\n",
+ "\n",
+ "\n",
+ "When a flowsheet contains a recycle loop, the outlet of a downstream unit becomes the inlet of an upstream unit, creating a cyclic dependency that prevents straightforward calculation of all stream conditions. The tear\u2010stream method is necessary because it \u201cbreaks\u201d this loop: you select one recycle stream as the tear, assign it an initial guess, and then solve the rest of the flowsheet as if it were acyclic. Once the downstream units compute their outputs, you compare the calculated value of the torn stream to your initial guess and iteratively adjust until they coincide. Without tearing, the solver cannot establish a proper topological sequence or drive the recycle to convergence, making initialization\u2014and ultimately steady\u2010state convergence\u2014impossible.\n",
+ "\n",
+ "It is important to determine the tear stream for a flowsheet which will be demonstrated below.\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "Currently, there are two methods of initializing a full flowsheet: using the sequential decomposition tool, or\n",
+ "manually propagating through the flowsheet. The tear stream in this example will be the stream from the mixer to the heater since that is where the\n",
+ "recycle stream first enters back into the main process.\n",
+ "\n"
+ ]
+ },
+ {
+ "metadata": {},
+ "cell_type": "markdown",
+ "source": "First, we will highlight some helpful functions that are used in the initialization process."
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.174558500Z",
+ "start_time": "2026-01-13T00:12:30.166118800Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "def initialize_unit(unit):\n",
+ " from idaes.core.util.exceptions import InitializationError\n",
+ " import idaes.logger as idaeslog\n",
+ "\n",
+ " optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 1000,\n",
+ " \"tol\": 1e-8,\n",
+ " }\n",
+ "\n",
+ " try:\n",
+ " initializer = unit.default_initializer(solver_options=optarg)\n",
+ " initializer.initialize(unit, output_level=idaeslog.INFO_LOW)\n",
+ " except InitializationError:\n",
+ " solver = get_solver(solver_options=optarg)\n",
+ " solver.solve(unit)"
+ ],
+ "outputs": [],
+ "execution_count": 39
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "This first function will take any unit model and can either initialize the model with its respective default\n",
+ "initializer, or use a generic solver and the solve the current state of the unit model. Often times a direct\n",
+ "initialization method will fail while a solving method will converge so having the option for both is helpful.\n",
+ "\n",
+ "\n",
+ "### 5.1 Sequential Decomposition\n",
+ "\n",
+ "This section will demonstrate how to use the built-in sequential decomposition tool to initialize our flowsheet. Sequential Decomposition is a tool from Pyomo where the documentation can be found here https://Pyomo.readthedocs.io/en/stable/explanation/modeling/network.html#sequential-decomposition"
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.183526400Z",
+ "start_time": "2026-01-13T00:12:30.175559400Z"
+ }
+ },
+ "cell_type": "code",
+ "source": [
+ "def automatic_propagation(m, tear_guesses):\n",
+ "\n",
+ " from pyomo.network import SequentialDecomposition\n",
+ "\n",
+ " seq = SequentialDecomposition()\n",
+ " seq.options.select_tear_method = \"heuristic\"\n",
+ " seq.options.tear_method = \"Wegstein\"\n",
+ " seq.options.iterLim = 5\n",
+ "\n",
+ " # Using the SD tool\n",
+ " G = seq.create_graph(m)\n",
+ " heuristic_tear_set = seq.tear_set_arcs(G, method=\"heuristic\")\n",
+ " order = seq.calculation_order(G)\n",
+ "\n",
+ " # Pass the tear_guess to the SD tool\n",
+ " seq.set_guesses_for(heuristic_tear_set[0].destination, tear_guesses)\n",
+ "\n",
+ " print(f\"Tear Stream starts at: {heuristic_tear_set[0].destination.name}\")\n",
+ "\n",
+ " for o in order:\n",
+ " print(o[0].name)\n",
+ "\n",
+ " seq.run(m, initialize_unit)"
+ ],
+ "outputs": [],
+ "execution_count": 40
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We are now ready to initialize our flowsheet in a sequential mode. Note that we specifically set the iteration limit\n",
+ "to be 5 as we are trying to use this tool only to get a good set of initial values such that IPOPT can then take over\n",
+ " and solve this flowsheet for us. Uncomment this function call to run the automatic propagation method"
+ ]
+ },
+ {
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.191140600Z",
+ "start_time": "2026-01-13T00:12:30.184028800Z"
+ }
+ },
+ "cell_type": "code",
+ "source": "# automatic_propagation(m, tear_guesses)",
+ "outputs": [],
+ "execution_count": 41
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 5.2 Manual Propagation Method\n",
+ "\n",
+ "This method uses a more direct approach to initialize the flowsheet, using the updated initializer method and propagating manually through the flowsheet and solving for the tear stream directly.\n",
+ "Lets define the function that will help us manually propagate and step through the flowsheet"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:30.202920300Z",
+ "start_time": "2026-01-13T00:12:30.191140600Z"
+ }
+ },
+ "source": [
+ "def manual_propagation(m, tear_guesses):\n",
+ " from idaes.core.util.initialization import propagate_state\n",
+ "\n",
+ " print(f\"The DOF is {degrees_of_freedom(m)} initially\")\n",
+ " m.fs.s03_expanded.deactivate()\n",
+ " print(f\"The DOF is {degrees_of_freedom(m)} after deactivating the tear stream\")\n",
+ "\n",
+ " for k, v in tear_guesses.items():\n",
+ " for k1, v1 in v.items():\n",
+ " getattr(m.fs.s03.destination, k)[k1].fix(v1)\n",
+ "\n",
+ " print(f\"The DOF is {degrees_of_freedom(m)} after setting the tear stream\")\n",
+ "\n",
+ " optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 300,\n",
+ " # \"tol\": 1e-10,\n",
+ " }\n",
+ "\n",
+ " solver = get_solver(solver_options=optarg)\n",
+ "\n",
+ " initialize_unit(m.fs.H101) # Initialize Heater\n",
+ " propagate_state(m.fs.s04) # Establish connection between Heater and Reactor\n",
+ " initialize_unit(m.fs.R101) # Initialize Reactor\n",
+ " propagate_state(\n",
+ " m.fs.s05\n",
+ " ) # Establish connection between Reactor and First Flash Unit\n",
+ " initialize_unit(m.fs.F101) # Initialize First Flash Unit\n",
+ " propagate_state(\n",
+ " m.fs.s06\n",
+ " ) # Establish connection between First Flash Unit and Splitter\n",
+ " propagate_state(\n",
+ " m.fs.s07\n",
+ " ) # Establish connection between First Flash Unit and Second Flash Unit\n",
+ " initialize_unit(m.fs.S101) # Initialize Splitter\n",
+ " propagate_state(m.fs.s08) # Establish connection between Splitter and Compressor\n",
+ " initialize_unit(m.fs.C101) # Initialize Compressor\n",
+ " propagate_state(m.fs.s09) # Establish connection between Compressor and Mixer\n",
+ " initialize_unit(m.fs.I101) # Initialize Toluene Inlet\n",
+ " propagate_state(m.fs.s01) # Establish connection between Toluene Inlet and Mixer\n",
+ " initialize_unit(m.fs.I102) # Initialize Hydrogen Inlet\n",
+ " propagate_state(m.fs.s02) # Establish connection between Hydrogen Inlet and Mixer\n",
+ " initialize_unit(m.fs.M101) # Initialize Mixer\n",
+ " propagate_state(m.fs.s03) # Establish connection between Mixer and Heater\n",
+ " solver.solve(m.fs.F102)\n",
+ " propagate_state(\n",
+ " m.fs.s10\n",
+ " ) # Establish connection between Second Flash Unit and Benzene Product\n",
+ " propagate_state(\n",
+ " m.fs.s11\n",
+ " ) # Establish connection between Second Flash Unit and Toluene Product\n",
+ " propagate_state(m.fs.s12) # Establish connection between Splitter and Purge Product\n",
+ "\n",
+ " optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 300,\n",
+ " \"tol\": 1e-8,\n",
+ " }\n",
+ " solver = get_solver(\"ipopt_v2\", options=optarg)\n",
+ " solver.solve(m, tee=False)\n",
+ "\n",
+ " for k, v in tear_guesses.items():\n",
+ " for k1, v1 in v.items():\n",
+ " getattr(m.fs.H101.inlet, k)[k1].unfix()\n",
+ "\n",
+ " m.fs.s03_expanded.activate()\n",
+ " print(\n",
+ " f\"The DOF is {degrees_of_freedom(m)} after unfixing the values and reactivating the tear stream\"\n",
+ " )"
+ ],
+ "outputs": [],
+ "execution_count": 42
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "It will first show that the degrees of freedom is correctly at 0 before any streams are deactivated. Once the tear\n",
+ "stream is deactivated though, the degrees of freedom will be 10. That means 10 variables will have to be defined with\n",
+ " the tear guesses `tear_guesses`. Then each unit model can be initialized with our same helper function and then can\n",
+ " propagate the corresponding connection to the following unit models. At the end, the whole flowsheet is solved,\n",
+ " giving a much better chance for the recycle stream to be used correctly the flowsheet to converge."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.448895Z",
+ "start_time": "2026-01-13T00:12:30.204922600Z"
+ }
+ },
+ "source": "manual_propagation(m, tear_guesses)",
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The DOF is 0 initially\n",
+ "The DOF is 10 after deactivating the tear stream\n",
+ "The DOF is 0 after setting the tear stream\n",
+ "The DOF is 0 after unfixing the values and reactivating the tear stream\n"
+ ]
+ }
+ ],
+ "execution_count": 43
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 6 Solving the Model"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "We have now initialized the flowsheet. Lets set up some solving options before simulating the flowsheet. We want to specify the scaling method, number of iterations, and tolerance. More specific or advanced options can be found at the documentation for IPOPT https://coin-or.github.io/Ipopt/OPTIONS.html"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.467353Z",
+ "start_time": "2026-01-13T00:12:32.458309200Z"
+ }
+ },
+ "source": [
+ "optarg = {\n",
+ " \"nlp_scaling_method\": \"user-scaling\",\n",
+ " \"OF_ma57_automatic_scaling\": \"yes\",\n",
+ " \"max_iter\": 1000,\n",
+ " \"tol\": 1e-8,\n",
+ "}"
+ ],
+ "outputs": [],
+ "execution_count": 44
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
+ "source": [
+ "
\n",
+ "Inline Exercise:\n",
+ "Let us run the flowsheet in a simulation mode to look at the results. To do this, complete the last line of code where we pass the model to the solver. You will need to type the following:\n",
+ "\n",
+ "solver = get_solver(solver_options=optarg)
\n",
+ "results = solver.solve(m, tee=True)\n",
+ "\n",
+ "Use Shift+Enter to run the cell once you have typed in your code.\n",
+ "
\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:32.476284300Z",
+ "start_time": "2026-01-13T00:12:32.467353Z"
+ }
+ },
+ "source": [
+ "# Create the solver object\n",
+ "\n",
+ "# Solve the model"
+ ],
+ "outputs": [],
+ "execution_count": 45
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 7 Analyze the results\n",
+ "\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "If the IDAES UI package was installed with the `idaes-pse` installation or installed separately, you can run the flowsheet visualizer to see a full diagram of the full process that is generated and displayed on a browser window.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "noauto"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.001529600Z",
+ "start_time": "2026-01-13T00:12:32.990965900Z"
+ }
+ },
+ "source": [
+ "# m.fs.visualize(\"HDA-Flowsheet\")"
+ ],
+ "outputs": [],
+ "execution_count": 48
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Otherwise, we can run the `m.fs.report()` method to see a full summary of the solved flowsheet. It is recommended to adjust the width of the output as much as possible for the cleanest display."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.046845800Z",
+ "start_time": "2026-01-13T00:12:33.009050800Z"
+ }
+ },
+ "source": [
+ "m.fs.report()"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "====================================================================================\n",
+ "Flowsheet : fs Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units s01 s02 s03 s04 s05 s06 s07 s08 s09 s10 s11 s12 \n",
+ " Total Molar Flowrate Liq mole / second 0.30001 2.0000e-05 0.34190 1.6073e-09 5.7340e-09 1.0000e-08 0.26712 1.1139e-06 1.1143e-06 1.0000e-08 0.094878 2.7856e-07\n",
+ " Total Molar Flowrate Vap mole / second 4.0000e-05 0.32002 1.6901 2.0320 2.0320 1.7648 1.0000e-08 1.4119 1.4119 0.17224 1.0000e-08 0.35297\n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 3.3332e-05 0.50000 0.22733 0.13374 0.63390 0.76595 0.76595 0.76595 0.76595 0.66001 0.66001 0.76595\n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.99997 0.50000 0.77267 0.86626 0.36610 0.23405 0.23405 0.23405 0.23405 0.33999 0.33999 0.23405\n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.25000 3.1248e-05 0.024624 0.058732 0.17408 0.084499 0.084499 0.084499 0.084499 0.82430 0.82430 0.084499\n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.25000 3.1248e-05 0.028601 0.15380 0.038450 0.0088437 0.0088437 0.0088435 0.0088435 0.17570 0.17570 0.0088435\n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.25000 0.93744 0.33283 0.27683 0.16148 0.18592 0.18592 0.18592 0.18592 1.7561e-08 1.7561e-08 0.18592\n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.25000 0.062496 0.61394 0.51064 0.62599 0.72074 0.72074 0.72074 0.72074 4.3265e-08 4.3265e-08 0.72074\n",
+ " Temperature kelvin 303.20 303.20 324.51 600.00 771.86 325.00 325.00 325.00 325.00 375.00 375.00 325.00\n",
+ " Pressure pascal 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 3.5000e+05 1.5000e+05 1.5000e+05 3.5000e+05\n",
+ "====================================================================================\n"
+ ]
+ }
+ ],
+ "execution_count": 49
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "What is the total operating cost?"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.065372900Z",
+ "start_time": "2026-01-13T00:12:33.055710300Z"
+ }
+ },
+ "source": [
+ "print(\"operating cost = $\", value(m.fs.operating_cost))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "operating cost = $ 419008.281895999\n"
+ ]
+ }
+ ],
+ "execution_count": 50
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "For this operating cost, what is the amount of benzene we are able to produce and what purity we are able to achieve? We can look at a specific unit models stream table with the same `report()` method."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.179064200Z",
+ "start_time": "2026-01-13T00:12:33.163247500Z"
+ }
+ },
+ "source": [
+ "m.fs.F102.report()\n",
+ "\n",
+ "print()\n",
+ "print(\"benzene purity = \", value(m.fs.purity))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.F102 Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : 7346.7 : watt : False : (None, None)\n",
+ " Pressure Change : -2.0000e+05 : pascal : True : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " Total Molar Flowrate Liq mole / second 0.26712 - - \n",
+ " Total Molar Flowrate Vap mole / second 1.0000e-08 - - \n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 0.76595 - - \n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.23405 - - \n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.084499 - - \n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.0088437 - - \n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.18592 - - \n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.72074 - - \n",
+ " Temperature kelvin 325.00 - - \n",
+ " Pressure pascal 3.5000e+05 - - \n",
+ " flow_mol_phase Liq mole / second - 1.0000e-08 0.094878 \n",
+ " flow_mol_phase Vap mole / second - 0.17224 1.0000e-08 \n",
+ " mole_frac_phase_comp ('Liq', 'benzene') dimensionless - 0.66001 0.66001 \n",
+ " mole_frac_phase_comp ('Liq', 'toluene') dimensionless - 0.33999 0.33999 \n",
+ " mole_frac_phase_comp ('Vap', 'benzene') dimensionless - 0.82430 0.82430 \n",
+ " mole_frac_phase_comp ('Vap', 'toluene') dimensionless - 0.17570 0.17570 \n",
+ " mole_frac_phase_comp ('Vap', 'hydrogen') dimensionless - 1.7561e-08 1.7561e-08 \n",
+ " mole_frac_phase_comp ('Vap', 'methane') dimensionless - 4.3265e-08 4.3265e-08 \n",
+ " temperature kelvin - 375.00 375.00 \n",
+ " pressure pascal - 1.5000e+05 1.5000e+05 \n",
+ "====================================================================================\n",
+ "\n",
+ "benzene purity = 0.8242963450787166\n"
+ ]
+ }
+ ],
+ "execution_count": 52
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Next, let's look at how much benzene we are losing with the light gases out of F101. IDAES has tools for creating stream tables based on the `Arcs` and/or `Ports` in a flowsheet. Let us create and print a simple stream table showing the stream leaving the reactor and the vapor stream from F101."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.290365100Z",
+ "start_time": "2026-01-13T00:12:33.276957500Z"
+ }
+ },
+ "source": [
+ "from idaes.core.util.tables import (\n",
+ " create_stream_table_dataframe,\n",
+ " stream_table_dataframe_to_string,\n",
+ ")\n",
+ "\n",
+ "st = create_stream_table_dataframe({\"Reactor\": m.fs.s05, \"Light Gases\": m.fs.s06})\n",
+ "print(stream_table_dataframe_to_string(st))"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ " Units Reactor Light Gases\n",
+ "Total Molar Flowrate Liq mole / second 5.7340e-09 1.0000e-08 \n",
+ "Total Molar Flowrate Vap mole / second 2.0320 1.7648 \n",
+ "Total Mole Fraction ('Liq', 'benzene') dimensionless 0.63390 0.76595 \n",
+ "Total Mole Fraction ('Liq', 'toluene') dimensionless 0.36610 0.23405 \n",
+ "Total Mole Fraction ('Vap', 'benzene') dimensionless 0.17408 0.084499 \n",
+ "Total Mole Fraction ('Vap', 'toluene') dimensionless 0.038450 0.0088437 \n",
+ "Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.16148 0.18592 \n",
+ "Total Mole Fraction ('Vap', 'methane') dimensionless 0.62599 0.72074 \n",
+ "Temperature kelvin 771.86 325.00 \n",
+ "Pressure pascal 3.5000e+05 3.5000e+05 \n"
+ ]
+ }
+ ],
+ "execution_count": 54
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## 8 Optimization\n",
+ "\n",
+ "\n",
+ "We saw from the results above that the total operating cost for the base case was $419,122 per year. We are producing 0.142 mol/s of benzene at a purity of 82\\%. However, we are losing around 42\\% of benzene in F101 vapor outlet stream. \n",
+ "\n",
+ "Let us try to minimize this cost such that:\n",
+ "- we are producing at least 0.15 mol/s of benzene in F102 vapor outlet i.e. our product stream\n",
+ "- purity of benzene i.e. the mole fraction of benzene in F102 vapor outlet is at least 80%\n",
+ "- restricting the benzene loss in F101 vapor outlet to less than 20%\n",
+ "\n",
+ "For this problem, our decision variables are as follows:\n",
+ "- H101 outlet temperature\n",
+ "- R101 cooling duty provided\n",
+ "- F101 outlet temperature\n",
+ "- F102 outlet temperature\n",
+ "- F102 deltaP in the flash tank\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us declare our objective function for this problem. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.307079900Z",
+ "start_time": "2026-01-13T00:12:33.298304Z"
+ }
+ },
+ "source": [
+ "m.fs.objective = Objective(expr=m.fs.operating_cost)"
+ ],
+ "outputs": [],
+ "execution_count": 55
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, we need to unfix the decision variables as we had solved a square problem (degrees of freedom = 0) until now. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.315754900Z",
+ "start_time": "2026-01-13T00:12:33.308100100Z"
+ }
+ },
+ "source": [
+ "m.fs.H101.outlet.temperature.unfix()\n",
+ "m.fs.R101.heat_duty.unfix()\n",
+ "m.fs.F101.vap_outlet.temperature.unfix()\n",
+ "m.fs.F102.vap_outlet.temperature.unfix()"
+ ],
+ "outputs": [],
+ "execution_count": 56
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
+ "source": [
+ "
\n",
+ "Inline Exercise:\n",
+ "Let us now unfix the remaining variable which is F102 pressure drop (F102.deltaP) \n",
+ "\n",
+ "Use Shift+Enter to run the cell once you have typed in your code. \n",
+ "
\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.324956200Z",
+ "start_time": "2026-01-13T00:12:33.315754900Z"
+ }
+ },
+ "source": [
+ "# Todo: Unfix deltaP for F102"
+ ],
+ "outputs": [],
+ "execution_count": 57
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Next, we need to set bounds on these decision variables to values shown below:\n",
+ "\n",
+ " - H101 outlet temperature [500, 600] K\n",
+ " - R101 outlet temperature [600, 800] K\n",
+ " - F101 outlet temperature [298, 450] K\n",
+ " - F102 outlet temperature [298, 450] K\n",
+ " - F102 outlet pressure [105000, 110000] Pa\n",
+ "\n",
+ "Let us first set the variable bound for the H101 outlet temperature as shown below:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.371189900Z",
+ "start_time": "2026-01-13T00:12:33.362714700Z"
+ }
+ },
+ "source": [
+ "m.fs.H101.outlet.temperature[0].setlb(500)\n",
+ "m.fs.H101.outlet.temperature[0].setub(600)"
+ ],
+ "outputs": [],
+ "execution_count": 60
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
+ "source": [
+ "
\n",
+ "Inline Exercise:\n",
+ "Now, set the variable bound for the R101 outlet temperature.\n",
+ "\n",
+ "Use Shift+Enter to run the cell once you have typed in your code. \n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.388084900Z",
+ "start_time": "2026-01-13T00:12:33.377308100Z"
+ }
+ },
+ "source": [
+ "# Todo: Set the bounds for reactor outlet temperature"
+ ],
+ "outputs": [],
+ "execution_count": 61
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us fix the bounds for the rest of the decision variables. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.426888400Z",
+ "start_time": "2026-01-13T00:12:33.413693900Z"
+ }
+ },
+ "source": [
+ "m.fs.F101.vap_outlet.temperature[0].setlb(298.0)\n",
+ "m.fs.F101.vap_outlet.temperature[0].setub(450.0)\n",
+ "m.fs.F102.vap_outlet.temperature[0].setlb(298.0)\n",
+ "m.fs.F102.vap_outlet.temperature[0].setub(450.0)\n",
+ "m.fs.F102.vap_outlet.pressure[0].setlb(105000)\n",
+ "m.fs.F102.vap_outlet.pressure[0].setub(110000)"
+ ],
+ "outputs": [],
+ "execution_count": 63
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now, the only things left to define are our constraints on overhead loss in F101, product flow rate and purity in F102. Let us first look at defining a constraint for the overhead loss in F101 where we are restricting the benzene leaving the vapor stream to less than 20 \\% of the benzene available in the reactor outlet. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.447188200Z",
+ "start_time": "2026-01-13T00:12:33.436145800Z"
+ }
+ },
+ "source": [
+ "m.fs.overhead_loss = Constraint(\n",
+ " expr=m.fs.F101.control_volume.properties_out[0].flow_mol_phase_comp[\n",
+ " \"Vap\", \"benzene\"\n",
+ " ]\n",
+ " <= 0.20\n",
+ " * m.fs.R101.control_volume.properties_out[0].flow_mol_phase_comp[\"Vap\", \"benzene\"]\n",
+ ")"
+ ],
+ "outputs": [],
+ "execution_count": 64
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ]
+ },
+ "source": [
+ "
\n",
+ "Inline Exercise:\n",
+ "Now, add the constraint such that we are producing at least 0.15 mol/s of benzene in the product stream which is the vapor outlet of F102. Let us name this constraint as m.fs.product_flow. \n",
+ "\n",
+ "Use Shift+Enter to run the cell once you have typed in your code. \n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "tags": [
+ "exercise"
+ ],
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.464672600Z",
+ "start_time": "2026-01-13T00:12:33.453771300Z"
+ }
+ },
+ "source": [
+ "# Todo: Add minimum product flow constraint"
+ ],
+ "outputs": [],
+ "execution_count": 65
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Let us add the final constraint on product purity or the mole fraction of benzene in the product stream such that it is at least greater than 80%. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.500096Z",
+ "start_time": "2026-01-13T00:12:33.487986500Z"
+ }
+ },
+ "source": [
+ "m.fs.product_purity = Constraint(expr=m.fs.purity >= 0.80)"
+ ],
+ "outputs": [],
+ "execution_count": 67
+ },
+ {
+ "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",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.630996900Z",
+ "start_time": "2026-01-13T00:12:33.501556Z"
+ }
+ },
+ "source": "results = solver.solve(m, tee=False)",
+ "outputs": [],
+ "execution_count": 68
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### 8.1 Optimization Results\n",
+ "\n",
+ "Display the results and product specifications"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.700975700Z",
+ "start_time": "2026-01-13T00:12:33.660461600Z"
+ }
+ },
+ "source": [
+ "print(\"operating cost = $\", value(m.fs.operating_cost))\n",
+ "\n",
+ "print()\n",
+ "print(\"Product flow rate and purity in F102\")\n",
+ "\n",
+ "m.fs.F102.report()\n",
+ "\n",
+ "print()\n",
+ "print(\"benzene purity = \", value(m.fs.purity))\n",
+ "\n",
+ "print()\n",
+ "print(\"Overhead loss in F101\")\n",
+ "m.fs.F101.report()"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "operating cost = $ 312674.2367537996\n",
+ "\n",
+ "Product flow rate and purity in F102\n",
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.F102 Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : 8370.2 : watt : False : (None, None)\n",
+ " Pressure Change : -2.4500e+05 : pascal : False : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " Total Molar Flowrate Liq mole / second 0.28812 - - \n",
+ " Total Molar Flowrate Vap mole / second 1.0000e-08 - - \n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 0.75463 - - \n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.24537 - - \n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.032748 - - \n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.0032478 - - \n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.21614 - - \n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.74786 - - \n",
+ " Temperature kelvin 301.88 - - \n",
+ " Pressure pascal 3.5000e+05 - - \n",
+ " flow_mol_phase Liq mole / second - 1.0000e-08 0.10493 \n",
+ " flow_mol_phase Vap mole / second - 0.18319 1.0000e-08 \n",
+ " mole_frac_phase_comp ('Liq', 'benzene') dimensionless - 0.64256 0.64256 \n",
+ " mole_frac_phase_comp ('Liq', 'toluene') dimensionless - 0.35744 0.35744 \n",
+ " mole_frac_phase_comp ('Vap', 'benzene') dimensionless - 0.81883 0.81883 \n",
+ " mole_frac_phase_comp ('Vap', 'toluene') dimensionless - 0.18117 0.18117 \n",
+ " mole_frac_phase_comp ('Vap', 'hydrogen') dimensionless - 1.1799e-08 1.1799e-08 \n",
+ " mole_frac_phase_comp ('Vap', 'methane') dimensionless - 4.0825e-08 4.0825e-08 \n",
+ " temperature kelvin - 362.93 362.93 \n",
+ " pressure pascal - 1.0500e+05 1.0500e+05 \n",
+ "====================================================================================\n",
+ "\n",
+ "benzene purity = 0.8188295888412465\n",
+ "\n",
+ "Overhead loss in F101\n",
+ "\n",
+ "====================================================================================\n",
+ "Unit : fs.F101 Time: 0.0\n",
+ "------------------------------------------------------------------------------------\n",
+ " Unit Performance\n",
+ "\n",
+ " Variables: \n",
+ "\n",
+ " Key : Value : Units : Fixed : Bounds\n",
+ " Heat Duty : -68347. : watt : False : (None, None)\n",
+ " Pressure Change : 0.0000 : pascal : True : (None, None)\n",
+ "\n",
+ "------------------------------------------------------------------------------------\n",
+ " Stream Table\n",
+ " Units Inlet Vapor Outlet Liquid Outlet\n",
+ " Total Molar Flowrate Liq mole / second 1.5819e-12 - - \n",
+ " Total Molar Flowrate Vap mole / second 1.9480 - - \n",
+ " Total Mole Fraction ('Liq', 'benzene') dimensionless 0.57381 - - \n",
+ " Total Mole Fraction ('Liq', 'toluene') dimensionless 0.42619 - - \n",
+ " Total Mole Fraction ('Vap', 'benzene') dimensionless 0.13952 - - \n",
+ " Total Mole Fraction ('Vap', 'toluene') dimensionless 0.039059 - - \n",
+ " Total Mole Fraction ('Vap', 'hydrogen') dimensionless 0.18417 - - \n",
+ " Total Mole Fraction ('Vap', 'methane') dimensionless 0.63725 - - \n",
+ " Temperature kelvin 775.95 - - \n",
+ " Pressure pascal 3.5000e+05 - - \n",
+ " flow_mol_phase Liq mole / second - 1.0000e-08 0.28812 \n",
+ " flow_mol_phase Vap mole / second - 1.6598 1.0000e-08 \n",
+ " mole_frac_phase_comp ('Liq', 'benzene') dimensionless - 0.75463 0.75463 \n",
+ " mole_frac_phase_comp ('Liq', 'toluene') dimensionless - 0.24537 0.24537 \n",
+ " mole_frac_phase_comp ('Vap', 'benzene') dimensionless - 0.032748 0.032748 \n",
+ " mole_frac_phase_comp ('Vap', 'toluene') dimensionless - 0.0032478 0.0032478 \n",
+ " mole_frac_phase_comp ('Vap', 'hydrogen') dimensionless - 0.21614 0.21614 \n",
+ " mole_frac_phase_comp ('Vap', 'methane') dimensionless - 0.74786 0.74786 \n",
+ " temperature kelvin - 301.88 301.88 \n",
+ " pressure pascal - 3.5000e+05 3.5000e+05 \n",
+ "====================================================================================\n"
+ ]
+ }
+ ],
+ "execution_count": 70
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Display optimal values for the decision variables"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "metadata": {
+ "ExecuteTime": {
+ "end_time": "2026-01-13T00:12:33.735319500Z",
+ "start_time": "2026-01-13T00:12:33.718148400Z"
+ }
+ },
+ "source": [
+ "print(\n",
+ " f\"\"\"Optimal Values:\n",
+ "\n",
+ "H101 outlet temperature = {value(m.fs.H101.outlet.temperature[0]):.3f} K\n",
+ "\n",
+ "R101 outlet temperature = {value(m.fs.R101.outlet.temperature[0]):.3f} K\n",
+ "\n",
+ "F101 outlet temperature = {value(m.fs.F101.vap_outlet.temperature[0]):.3f} K\n",
+ "\n",
+ "F102 outlet temperature = {value(m.fs.F102.vap_outlet.temperature[0]):.3f} K\n",
+ "F102 outlet pressure = {value(m.fs.F102.vap_outlet.pressure[0]):.3f} Pa\n",
+ "\"\"\"\n",
+ ")"
+ ],
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Optimal Values:\n",
+ "\n",
+ "H101 outlet temperature = 500.000 K\n",
+ "\n",
+ "R101 outlet temperature = 775.947 K\n",
+ "\n",
+ "F101 outlet temperature = 301.881 K\n",
+ "\n",
+ "F102 outlet temperature = 362.935 K\n",
+ "F102 outlet pressure = 105000.000 Pa\n",
+ "\n"
+ ]
+ }
+ ],
+ "execution_count": 72
+ }
+ ],
+ "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.12.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 3
}
\ No newline at end of file
diff --git a/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_solution.ipynb b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_solution.ipynb
index 088af81e..a79f75a0 100644
--- a/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_solution.ipynb
+++ b/idaes_examples/notebooks/docs/tut/core/hda_flowsheet_solution.ipynb
@@ -1,1483 +1,2509 @@
{
- "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",
- "# HDA Flowsheet Simulation and Optimization\n",
- "\n",
- "Author: Jaffer Ghouse \n",
- "Maintainer: Brandon Paul \n",
- "Updated: 2023-06-01 \n",
- "\n",
- "## Learning outcomes\n",
- "\n",
- "\n",
- "- Construct a steady-state flowsheet using the IDAES unit model library\n",
- "- Connecting unit models in a flowsheet using Arcs\n",
- "- Using the SequentialDecomposition tool to initialize a flowsheet with recycle\n",
- "- Formulate 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",
- "Hydrodealkylation is a chemical reaction that often involves reacting\n",
- "an aromatic hydrocarbon in the presence of hydrogen gas to form a\n",
- "simpler aromatic hydrocarbon devoid of functional groups. In this\n",
- "example, toluene will be reacted with hydrogen gas at high temperatures\n",
- " to form benzene via the following reaction:\n",
- "\n",
- "**C
6H
5CH
3 + H
2 → C
6H
6 + CH
4**\n",
- "\n",
- "\n",
- "This reaction is often accompanied by an equilibrium side reaction\n",
- "which forms diphenyl, which we will neglect for this example.\n",
- "\n",
- "This example is based on the 1967 AIChE Student Contest problem as\n",
- "present by Douglas, J.M., Chemical Design of Chemical Processes, 1988,\n",
- "McGraw-Hill.\n",
- "\n",
- "The flowsheet that we will be using for this module is shown below with the stream conditions. We will be processing toluene and hydrogen to produce at least 370 TPY of benzene. As shown in the flowsheet, there are two flash tanks, F101 to separate out the non-condensibles and F102 to further separate the benzene-toluene mixture to improve the benzene purity. Note that typically a distillation column is required to obtain high purity benzene but that is beyond the scope of this workshop. The non-condensibles separated out in F101 will be partially recycled back to M101 and the rest will be either purged or combusted for power generation.We will assume ideal gas for this flowsheet. The properties required for this module are available in the same directory:\n",
- "\n",
- "- hda_ideal_VLE.py\n",
- "- hda_reaction.py\n",
- "\n",
- "The state variables chosen for the property package are **flows of component by phase, temperature and pressure**. The components considered are: **toluene, hydrogen, benzene and methane**. Therefore, every stream has 8 flow variables, 1 temperature and 1 pressure variable. \n",
- "\n",
- "\n",
- "\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 package. 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",
- "- SolverFactory (to solve the problem)\n",
- "- TransformationFactory (to apply certain transformations)\n",
- "- Arc (to connect two unit models)\n",
- "- SequentialDecomposition (to initialize the flowsheet in a sequential mode)\n",
- "\n",
- "For further details on these components, please refer to the pyomo documentation: https://pyomo.readthedocs.io/en/stable/\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",
- " SolverFactory,\n",
- " TransformationFactory,\n",
- " value,\n",
- ")\n",
- "from pyomo.network import Arc, SequentialDecomposition"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "From idaes, we will be needing the FlowsheetBlock and the following unit models:\n",
- "- Mixer\n",
- "- Heater\n",
- "- StoichiometricReactor\n",
- "-
**Flash**\n",
- "- Separator (splitter) \n",
- "- PressureChanger"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.core import FlowsheetBlock"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.models.unit_models import (\n",
- " PressureChanger,\n",
- " Mixer,\n",
- " Separator as Splitter,\n",
- " Heater,\n",
- " StoichiometricReactor,\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "Now, import the remaining unit models highlighted in blue above and run the cell using `Shift+Enter` after typing in the code. \n",
- "
\n"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "exercise"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: import flash model from idaes.models.unit_models"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: import flash model from idaes.models.unit_models\n",
- "from idaes.models.unit_models import Flash"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We will also be needing some utility tools to put together the flowsheet and calculate the degrees of freedom. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.models.unit_models.pressure_changer import ThermodynamicAssumption\n",
- "from idaes.core.util.model_statistics import degrees_of_freedom\n",
- "\n",
- "# Import idaes logger to set output levels\n",
- "import idaes.logger as idaeslog\n",
- "from idaes.core.solvers import get_solver\n",
- "from idaes.core.util.exceptions import InitializationError"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Importing required thermo and reaction package\n",
- "\n",
- "The final set of imports are to import the thermo and reaction package for the HDA process. We have created a custom thermo package that assumes Ideal Gas with support for VLE. \n",
- "\n",
- "The reaction package here is very simple as we will be using only a StochiometricReactor and the reaction package consists of the stochiometric coefficients for the reaction and the parameter for the heat of reaction. \n",
- "\n",
- "Let us import the following modules and they are in the same directory as this jupyter notebook:\n",
- "
\n",
- " - hda_ideal_VLE as thermo_props
\n",
- " - hda_reaction as reaction_props
\n",
- "
\n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes_examples.mod.hda import hda_ideal_VLE as thermo_props\n",
- "from idaes_examples.mod.hda import hda_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 as we did in module 1. "
- ]
- },
- {
- "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 Module 1, where we only had a thermo property package, for this flowsheet we will also need to add a reaction property package. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.thermo_params = thermo_props.HDAParameterBlock()\n",
- "m.fs.reaction_params = reaction_props.HDAReactionParameterBlock(\n",
- " property_package=m.fs.thermo_params\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 the Mixer (assigned a name M101) and a Heater (assigned a name H101). 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 (https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/unit_models/index.html). For example, the Mixer unit model here is given a `list` consisting of names to the three inlets. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "m.fs.M101 = Mixer(\n",
- " property_package=m.fs.thermo_params,\n",
- " inlet_list=[\"toluene_feed\", \"hydrogen_feed\", \"vapor_recycle\"],\n",
- ")\n",
- "\n",
- "m.fs.H101 = Heater(\n",
- " property_package=m.fs.thermo_params,\n",
- " has_pressure_change=False,\n",
- " has_phase_equilibrium=True,\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "