diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model.ipynb b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model.ipynb index 7465fdda..ace8076d 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 29, "id": "4a907cd6", "metadata": {}, "outputs": [], @@ -64,7 +64,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 30, "id": "cde159f7", "metadata": {}, "outputs": [], @@ -130,7 +130,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 31, "id": "4f7ace27", "metadata": {}, "outputs": [], @@ -223,7 +223,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 32, "id": "da8106f0", "metadata": {}, "outputs": [], @@ -268,7 +268,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 33, "id": "9040b578", "metadata": {}, "outputs": [], @@ -379,7 +379,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 34, "id": "e7257f0b", "metadata": {}, "outputs": [], @@ -605,7 +605,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 35, "id": "8f1d9869", "metadata": {}, "outputs": [], @@ -666,7 +666,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 36, "id": "943ae4f2", "metadata": {}, "outputs": [], @@ -834,7 +834,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 37, "id": "c8c84c75", "metadata": {}, "outputs": [], @@ -1063,10 +1063,103 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 38, "id": "f8e9968b", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "Ipopt 3.13.2: linear_solver=\"ma57\"\n", + "max_iter=200\n", + "nlp_scaling_method=\"gradient-based\"\n", + "tol=1e-08\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...: 6\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 0\n", + "\n", + "Total number of variables............................: 6\n", + " variables with only lower bounds: 6\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.98e+01 -1.0 4.10e-01 - 1.00e+00 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.05e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.97e+04 1.6 3.67e+02 - 1.00e+00 2.30e-03f 1\n", + " 5r 0.0000000e+00 4.00e+01 1.13e+02 1.6 3.89e-01 - 9.97e-01 1.00e+00f 1\n", + " 6r 0.0000000e+00 4.00e+01 1.13e+00 -1.2 8.42e-02 - 9.92e-01 9.87e-01f 1\n", + " 7r 0.0000000e+00 4.00e+01 9.78e-04 -4.3 8.94e-04 - 1.00e+00 1.00e+00f 1\n", + " 8r 0.0000000e+00 4.00e+01 5.79e-05 -6.4 1.49e-07 - 1.00e+00 1.00e+00f 1\n", + " 9r 0.0000000e+00 4.00e+01 5.20e-06 -9.0 6.12e-10 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 9\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 2.5739427655935287e-07 2.5739427655935287e-07\n", + "Constraint violation....: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "Complementarity.........: 9.0909090910996662e-10 9.0909090910996662e-10\n", + "Overall NLP error.......: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 15\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 15\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 12\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "Could not solve fs.lex after block triangularization finished.\n" + ] + } + ], "source": [ "import pyomo.environ as pyo\n", "from idaes.core import FlowsheetBlock\n", @@ -1126,7 +1219,10 @@ "\n", "def initialize_model(m):\n", " initializer = BlockTriangularizationInitializer()\n", - " initializer.initialize(m.fs.lex)\n", + " try:\n", + " initializer.initialize(m.fs.lex)\n", + " except InitializationError as err:\n", + " print(err)\n", " return m\n", "\n", "\n", @@ -1137,7 +1233,7 @@ " return m\n", "\n", "\n", - "if __name__ == main:\n", + "if __name__ == \"__main__\":\n", " main()" ] }, @@ -1148,14 +1244,14 @@ "source": [ "# 4. Model Diagnostics using DiagnosticsToolbox\n", "\n", - "Here, during initialization, we encounter warnings indicating that variables are being set to negative values, which is not expected behavior. These warnings suggest that there may be flaws in the model that require further investigation using the DiagnosticsToolbox from IDAES. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter.ipynb).\n", + "Here, during initialization, we encounter warnings indicating that variables are being set to negative values before an exception is raised stating that solving the model failed. These issues suggest that there may be flaws in the model that require further investigation using the DiagnosticsToolbox from IDAES. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter.ipynb).\n", "\n", "To proceed with investigating these issues, we need to import the DiagnosticsToolbox. We can gain a better understanding of its functionality by running the help function on it. " ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 39, "id": "27f7d6c9", "metadata": {}, "outputs": [], @@ -1185,7 +1281,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 40, "id": "307ec415", "metadata": {}, "outputs": [ @@ -1205,6 +1301,80 @@ "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", "`-0.05` (float) not in domain NonNegativeReals.\n", " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "Ipopt 3.13.2: linear_solver=\"ma57\"\n", + "max_iter=200\n", + "nlp_scaling_method=\"gradient-based\"\n", + "tol=1e-08\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...: 6\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 0\n", + "\n", + "Total number of variables............................: 6\n", + " variables with only lower bounds: 6\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.98e+01 -1.0 4.10e-01 - 1.00e+00 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.05e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.97e+04 1.6 3.67e+02 - 1.00e+00 2.30e-03f 1\n", + " 5r 0.0000000e+00 4.00e+01 1.13e+02 1.6 3.89e-01 - 9.97e-01 1.00e+00f 1\n", + " 6r 0.0000000e+00 4.00e+01 1.13e+00 -1.2 8.42e-02 - 9.92e-01 9.87e-01f 1\n", + " 7r 0.0000000e+00 4.00e+01 9.78e-04 -4.3 8.94e-04 - 1.00e+00 1.00e+00f 1\n", + " 8r 0.0000000e+00 4.00e+01 5.79e-05 -6.4 1.49e-07 - 1.00e+00 1.00e+00f 1\n", + " 9r 0.0000000e+00 4.00e+01 5.20e-06 -9.0 6.12e-10 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 9\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 2.5739427655935287e-07 2.5739427655935287e-07\n", + "Constraint violation....: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "Complementarity.........: 9.0909090910996662e-10 9.0909090910996662e-10\n", + "Overall NLP error.......: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 15\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 15\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 12\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "Could not solve fs.lex after block triangularization finished.\n", "====================================================================================\n", "Model Statistics\n", "\n", @@ -1253,7 +1423,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 41, "id": "28cb6a6a", "metadata": {}, "outputs": [ @@ -1329,7 +1499,7 @@ "Number of equality constraint Jacobian evaluations = 14\n", "Number of inequality constraint Jacobian evaluations = 0\n", "Number of Lagrangian Hessian evaluations = 12\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", "Total CPU secs in NLP function evaluations = 0.000\n", "\n", "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", @@ -1343,10 +1513,10 @@ { "data": { "text/plain": [ - "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06552338600158691}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06122612953186035}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" ] }, - "execution_count": 13, + "execution_count": 41, "metadata": {}, "output_type": "execute_result" } @@ -1366,7 +1536,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 42, "id": "2c5af45e", "metadata": {}, "outputs": [ @@ -1396,6 +1566,7 @@ "Suggested next steps:\n", "\n", " display_constraints_with_large_residuals()\n", + " compute_infeasibility_explanation()\n", " display_variables_at_or_outside_bounds()\n", "\n", "====================================================================================\n" @@ -1424,7 +1595,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 43, "id": "465e5788", "metadata": {}, "outputs": [ @@ -1463,7 +1634,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 44, "id": "46fd1bfd", "metadata": {}, "outputs": [ @@ -1499,7 +1670,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 45, "id": "273cadde", "metadata": {}, "outputs": [ @@ -1520,7 +1691,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 46, "id": "1f4b3998", "metadata": {}, "outputs": [ @@ -1529,7 +1700,7 @@ "output_type": "stream", "text": [ "{Member of conc_mass_comp} : Component mass concentrations\n", - " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", + " Size=3, Index=fs.aq_properties.solute_set, Units=g/l\n", " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", " NaCl : 0 : 0.15 : None : True : True : NonNegativeReals\n", "flow_vol : Total volumetric flowrate\n", @@ -1537,7 +1708,7 @@ " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", " None : 0 : 100.0 : None : True : True : NonNegativeReals\n", "{Member of conc_mass_comp} : Component mass concentrations\n", - " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", + " Size=3, Index=fs.aq_properties.solute_set, Units=g/l\n", " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", " NaCl : 0 : 0.0 : None : False : False : NonNegativeReals\n", "flow_vol : Total volumetric flowrate\n", @@ -1571,7 +1742,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 47, "id": "6d67dcbe", "metadata": {}, "outputs": [ @@ -1600,7 +1771,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 48, "id": "a29bad38", "metadata": {}, "outputs": [], @@ -1629,7 +1800,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 49, "id": "955afa2e", "metadata": {}, "outputs": [ @@ -1683,7 +1854,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 50, "id": "dd5ae6d0", "metadata": {}, "outputs": [ @@ -1748,7 +1919,7 @@ "Number of equality constraint Jacobian evaluations = 2\n", "Number of inequality constraint Jacobian evaluations = 0\n", "Number of Lagrangian Hessian evaluations = 1\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.001\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", "Total CPU secs in NLP function evaluations = 0.000\n", "\n", "EXIT: Optimal Solution Found.\n" @@ -1757,10 +1928,10 @@ { "data": { "text/plain": [ - "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.07779264450073242}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.0668952465057373}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" ] }, - "execution_count": 22, + "execution_count": 50, "metadata": {}, "output_type": "execute_result" } @@ -1824,7 +1995,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 51, "id": "4bab5ea0", "metadata": {}, "outputs": [], @@ -1889,7 +2060,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 52, "id": "1c27f9a5", "metadata": {}, "outputs": [], @@ -1947,7 +2118,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 53, "id": "a6f150be", "metadata": {}, "outputs": [], @@ -1971,7 +2142,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 54, "id": "e0ba8e4e", "metadata": {}, "outputs": [], @@ -2036,7 +2207,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 55, "id": "6e61ccb6", "metadata": {}, "outputs": [], @@ -2135,7 +2306,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 56, "id": "ca1e499b", "metadata": {}, "outputs": [], @@ -2273,7 +2444,7 @@ ], "metadata": { "kernelspec": { - "display_name": "idaes-pse", + "display_name": "examples-310-new", "language": "python", "name": "python3" }, @@ -2287,7 +2458,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.13" + "version": "3.10.19" } }, "nbformat": 4, diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_doc.ipynb b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_doc.ipynb index cdbbb12c..a2c3c1e3 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_doc.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_doc.ipynb @@ -1,2245 +1,2408 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "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": [ - "# Creating Custom Unit Model\n", - "Author: Javal Vyas \n", - "Maintainer: Javal Vyas \n", - "Updated: 2023-02-20\n", - "\n", - "This tutorial is a comprehensive step-wise procedure to build a custom unit model from scratch. This tutorial will include creating a property package, a custom unit model and testing them. For this tutorial we shall create a custom unit model for Liquid - Liquid Extraction. \n", - "\n", - "The Liquid - Liquid Extractor model contains two immiscible fluids forming the two phases. One of the phases, say phase_1 has a high concentration of solutes which is to be separated. A mass transfer happens between the two phases and the solute is transferred from phase_1 to phase_2. This mass transfer is governed by a parameter called the distribution coefficient.\n", - "\n", - "After reviewing the working principles of the Liquid - Liquid Extractor, we shall proceed to create a custom unit model. We will require a property package for each phase, a custom unit model class and tests for the model and property packages.\n", - "\n", - "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", - "- Steady-state only\n", - "- Organic phase property package has a single phase named Org\n", - "- Aqueous phase property package has a single phase named Aq\n", - "- Organic and Aqueous phase properties need not have the same component list. \n", - "\n", - "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1. Creating Organic Property Package\n", - "\n", - "Creating a property package is a 4 step process\n", - "- Import necessary libraries \n", - "- Creating Physical Parameter Data Block\n", - "- Define State Block\n", - "- Define State Block Data\n", - "\n", - "# 1.1 Importing necessary packages \n", - "Let us begin with the importing the necessary libraries where we will be using functionalities from IDAES and Pyomo. " - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# Import Python libraries\n", - "import logging\n", - "\n", - "import idaes.logger as idaeslog\n", - "from idaes.core.util.initialization import fix_state_vars\n", - "\n", - "# Import Pyomo libraries\n", - "from pyomo.environ import (\n", - " Param,\n", - " Set,\n", - " Var,\n", - " NonNegativeReals,\n", - " units,\n", - " Expression,\n", - " PositiveReals,\n", - ")\n", - "\n", - "# Import IDAES cores\n", - "from idaes.core import (\n", - " declare_process_block_class,\n", - " MaterialFlowBasis,\n", - " PhysicalParameterBlock,\n", - " StateBlockData,\n", - " StateBlock,\n", - " MaterialBalanceType,\n", - " EnergyBalanceType,\n", - " Solute,\n", - " Solvent,\n", - " LiquidPhase,\n", - ")\n", - "from idaes.core.util.model_statistics import degrees_of_freedom" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1.2 Physical Parameter Data Block\n", - "\n", - "A `PhysicalParameterBlock` serves as the central point of reference for all aspects of the property package and needs to define several things about the package. These are summarized below:\n", - "\n", - "- Units of measurement\n", - "- What properties are supported and how they are implemented\n", - "- What components and phases are included in the packages\n", - "- All the global parameters necessary for calculating properties\n", - "- A reference to the associated State Block class, so that construction of the State Block components can be automated from the Physical Parameter Block\n", - "\n", - "To construct this block, we begin by declaring a process block class using a Python decorator. One can learn more about `declare_process_block_class` [here](https://github.com/IDAES/idaes-pse/blob/eea1209077b75f7d940d8958362e69d4650c079d/idaes/core/base/process_block.py#L173). After constructing the process block, we define a build function which contains all the components that the property package would have. `super` function here is used to give access to methods and properties of a parent or sibling class and since this is used on the class `PhysicalParameterData` class, build has access to all the parent and sibling class methods. \n", - "\n", - "The `PhysicalParameterBlock` then refers to the `state block`, in this case `OrgPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we move on to list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. Like here since the solvent is in the Organic phase, we will assign the Phase as OrganicPhase and the variable will be named Org as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", - " \n", - "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the Organic phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. Additionally, a parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", - "\n", - "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). In this method, we will call the predefined method `add_default_units()`.\n", - "\n", - "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default. " - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "@declare_process_block_class(\"OrgPhase\")\n", - "class PhysicalParameterData(PhysicalParameterBlock):\n", - " \"\"\"\n", - " Property Parameter Block Class\n", - "\n", - " Contains parameters and indexing sets associated with properties for\n", - " organic Phase\n", - "\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction.\n", - " \"\"\"\n", - " super().build()\n", - "\n", - " self._state_block_class = OrgPhaseStateBlock\n", - "\n", - " # List of valid phases in property package\n", - " self.Org = LiquidPhase()\n", - "\n", - " # Component list - a list of component identifiers\n", - " self.NaCl = Solute()\n", - " self.KNO3 = Solute()\n", - " self.CaSO4 = Solute()\n", - " self.solvent = (\n", - " Solvent()\n", - " ) # Solvent used here is ethylene dibromide (Organic Polar)\n", - "\n", - " # Heat capacity of solvent\n", - " self.cp_mass = Param(\n", - " mutable=True,\n", - " initialize=717.01,\n", - " doc=\"Specific heat capacity of solvent\",\n", - " units=units.J / units.kg / units.K,\n", - " )\n", - "\n", - " self.dens_mass = Param(\n", - " mutable=True,\n", - " initialize=2170,\n", - " doc=\"Density of ethylene dibromide\",\n", - " units=units.kg / units.m**3,\n", - " )\n", - " self.temperature_ref = Param(\n", - " within=PositiveReals,\n", - " mutable=True,\n", - " default=298.15,\n", - " doc=\"Reference temperature\",\n", - " units=units.K,\n", - " )\n", - " self.diffusion_factor = Param(\n", - " self.solute_set,\n", - " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", - " within=PositiveReals,\n", - " mutable=True,\n", - " )\n", - "\n", - " @classmethod\n", - " def define_metadata(cls, obj):\n", - " obj.add_default_units(\n", - " {\n", - " \"time\": units.hour,\n", - " \"length\": units.m,\n", - " \"mass\": units.g,\n", - " \"amount\": units.mol,\n", - " \"temperature\": units.K,\n", - " }\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1.3 State Block\n", - "\n", - "After the `PhysicalParameterBlock` class has been created, the next step is to write the code necessary to create the State Blocks that will be used throughout the flowsheet. `StateBlock` contains all the information necessary to define the state of the system. This includes the state variables and constraints on those variables which are used to describe a state property like the enthalpy, material balance, etc.\n", - "\n", - "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed StateBlock all at once (rather than element by element).\n", - "\n", - "The class `_OrganicStateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the `fix_initialization_states` function. `fix_initialization_states` function is to used to fix the state variable within the state block with the provided initial values (usually inlet conditions). It takes a `block` as the argument in which the state variables are to be fixed. It also takes `state_args` as an optional argument. `state_args` is a dictionary with the value for the state variables to be fixed. This function returns a dictionary indexed by the block, state variables and variable index indicating the fixed status of each variable before applying the function. \n", - "\n", - "The above function comprise of the _OrganicStateBlock, next we shall see the construction of the OrgPhaseStateBlockData class." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "class _OrganicStateBlock(StateBlock):\n", - " \"\"\"\n", - " This Class contains methods which should be applied to Property Blocks as a\n", - " whole, rather than individual elements of indexed Property Blocks.\n", - " \"\"\"\n", - "\n", - " def fix_initialization_states(self):\n", - " fix_state_vars(self)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The class `OrgPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `OrgPhaseStateBlock`, and inherits the block class from `_OrganicStateBlock`. This inheritance allows `OrgPhaseStateBlockData` to leverage functions from `_OrganicStateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", - "\n", - "The subsequent objective is to delineate the state variables, accomplished through the `_make_state_vars` method. This method encompasses all the essential state variables and associated data. For this particular property package, the required state variables are:\n", - "\n", - "- `flow_vol` - volumetric flow rate\n", - "- `conc_mass_comp` - mass fractions\n", - "- `pressure` - state pressure\n", - "- `temperature` - state temperature\n", - "\n", - "After establishing the state variables, the subsequent step involves setting up state properties as constraints. This includes specifying the relationships and limitations that dictate the system's behavior. The following properties need to be articulated:\n", - "\n", - "-`get_material_flow_terms`: quantifies the amount of material flow.\n", - "- `get_enthalpy_flow_terms`: quantifies the amount of enthalpy flow.\n", - "- `get_flow_rate`: details volumetric flow rates.\n", - "- `default_material_balance_type`: defines the kind of material balance to be used.\n", - "- `default_energy_balance_type`: defines the kind of energy balance to be used.\n", - "- `define_state_vars`: involves defining state variables with units, akin to the define_metadata function in the PhysicalParameterData block.\n", - "- `get_material_flow_basis`: establishes the basis on which state variables are measured, whether in mass or molar terms.\n", - "\n", - "These definitions mark the conclusion of the state block construction and thus the property package. For additional details on creating a property package, please refer to this [resource](../../properties/custom/custom_physical_property_packages_doc.md ).\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_OrganicStateBlock)\n", - "class OrgPhaseStateBlockData(StateBlockData):\n", - " \"\"\"\n", - " An example property package for Organic phzase for liquid liquid extraction\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction\n", - " \"\"\"\n", - " super().build()\n", - " self._make_state_vars()\n", - "\n", - " def _make_state_vars(self):\n", - " self.flow_vol = Var(\n", - " initialize=1,\n", - " domain=NonNegativeReals,\n", - " doc=\"Total volumetric flowrate\",\n", - " units=units.L / units.hour,\n", - " )\n", - " self.conc_mass_comp = Var(\n", - " self.params.solute_set,\n", - " domain=NonNegativeReals,\n", - " initialize=1,\n", - " doc=\"Component mass concentrations\",\n", - " units=units.g / units.L,\n", - " )\n", - " self.pressure = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=1,\n", - " bounds=(1, 5),\n", - " units=units.atm,\n", - " doc=\"State pressure [atm]\",\n", - " )\n", - "\n", - " self.temperature = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=300,\n", - " bounds=(273, 373),\n", - " units=units.K,\n", - " doc=\"State temperature [K]\",\n", - " )\n", - "\n", - " def material_flow_expression(self, j):\n", - " if j == \"solvent\":\n", - " return self.flow_vol * self.params.dens_mass\n", - " else:\n", - " return self.flow_vol * self.conc_mass_comp[j]\n", - "\n", - " self.material_flow_expression = Expression(\n", - " self.component_list,\n", - " rule=material_flow_expression,\n", - " doc=\"Material flow terms\",\n", - " )\n", - "\n", - " def enthalpy_flow_expression(self):\n", - " return (\n", - " self.flow_vol\n", - " * self.params.dens_mass\n", - " * self.params.cp_mass\n", - " * (self.temperature - self.params.temperature_ref)\n", - " )\n", - "\n", - " self.enthalpy_flow_expression = Expression(\n", - " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", - " )\n", - "\n", - " def get_flow_rate(self):\n", - " return self.flow_vol\n", - "\n", - " def get_material_flow_terms(self, p, j):\n", - " return self.material_flow_expression[j]\n", - "\n", - " def get_enthalpy_flow_terms(self, p):\n", - " return self.enthalpy_flow_expression\n", - "\n", - " def default_material_balance_type(self):\n", - " return MaterialBalanceType.componentTotal\n", - "\n", - " def default_energy_balance_type(self):\n", - " return EnergyBalanceType.enthalpyTotal\n", - "\n", - " def define_state_vars(self):\n", - " return {\n", - " \"flow_vol\": self.flow_vol,\n", - " \"conc_mass_comp\": self.conc_mass_comp,\n", - " \"temperature\": self.temperature,\n", - " \"pressure\": self.pressure,\n", - " }\n", - "\n", - " def get_material_flow_basis(self):\n", - " return MaterialFlowBasis.mass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 2. Creating Aqueous Property Package\n", - "\n", - "The structure of Aqueous Property Package mirrors that of the Organic Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "# Import Python libraries\n", - "import logging\n", - "\n", - "from idaes.core.util.initialization import fix_state_vars\n", - "\n", - "# Import Pyomo libraries\n", - "from pyomo.environ import (\n", - " Param,\n", - " Var,\n", - " NonNegativeReals,\n", - " units,\n", - " Expression,\n", - " PositiveReals,\n", - ")\n", - "\n", - "# Import IDAES cores\n", - "from idaes.core import (\n", - " declare_process_block_class,\n", - " MaterialFlowBasis,\n", - " PhysicalParameterBlock,\n", - " StateBlockData,\n", - " StateBlock,\n", - " MaterialBalanceType,\n", - " EnergyBalanceType,\n", - " Solute,\n", - " Solvent,\n", - " LiquidPhase,\n", - ")\n", - "\n", - "# Some more information about this module\n", - "__author__ = \"Javal Vyas\"\n", - "\n", - "\n", - "# Set up logger\n", - "_log = logging.getLogger(__name__)\n", - "\n", - "\n", - "@declare_process_block_class(\"AqPhase\")\n", - "class AqPhaseData(PhysicalParameterBlock):\n", - " \"\"\"\n", - " Property Parameter Block Class\n", - "\n", - " Contains parameters and indexing sets associated with properties for\n", - " aqueous Phase\n", - "\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction.\n", - " \"\"\"\n", - " super().build()\n", - "\n", - " self._state_block_class = AqPhaseStateBlock\n", - "\n", - " # List of valid phases in property package\n", - " self.Aq = LiquidPhase()\n", - "\n", - " # Component list - a list of component identifiers\n", - " self.NaCl = Solute()\n", - " self.KNO3 = Solute()\n", - " self.CaSO4 = Solute()\n", - " self.H2O = Solvent()\n", - "\n", - " # Heat capacity of solvent\n", - " self.cp_mass = Param(\n", - " mutable=True,\n", - " initialize=4182,\n", - " doc=\"Specific heat capacity of solvent\",\n", - " units=units.J / units.kg / units.K,\n", - " )\n", - "\n", - " self.dens_mass = Param(\n", - " mutable=True,\n", - " initialize=997,\n", - " doc=\"Density of ethylene dibromide\",\n", - " units=units.kg / units.m**3,\n", - " )\n", - " self.temperature_ref = Param(\n", - " within=PositiveReals,\n", - " mutable=True,\n", - " default=298.15,\n", - " doc=\"Reference temperature\",\n", - " units=units.K,\n", - " )\n", - "\n", - " @classmethod\n", - " def define_metadata(cls, obj):\n", - " obj.add_default_units(\n", - " {\n", - " \"time\": units.hour,\n", - " \"length\": units.m,\n", - " \"mass\": units.g,\n", - " \"amount\": units.mol,\n", - " \"temperature\": units.K,\n", - " }\n", - " )\n", - "\n", - "\n", - "class _AqueousStateBlock(StateBlock):\n", - " \"\"\"\n", - " This Class contains methods which should be applied to Property Blocks as a\n", - " whole, rather than individual elements of indexed Property Blocks.\n", - " \"\"\"\n", - "\n", - " def fix_initialization_states(self):\n", - " fix_state_vars(self)\n", - "\n", - "\n", - "@declare_process_block_class(\"AqPhaseStateBlock\", block_class=_AqueousStateBlock)\n", - "class AqPhaseStateBlockData(StateBlockData):\n", - " \"\"\"\n", - " An example property package for ideal gas properties with Gibbs energy\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction\n", - " \"\"\"\n", - " super().build()\n", - " self._make_state_vars()\n", - "\n", - " def _make_state_vars(self):\n", - " self.flow_vol = Var(\n", - " initialize=1,\n", - " domain=NonNegativeReals,\n", - " doc=\"Total volumetric flowrate\",\n", - " units=units.L / units.hour,\n", - " )\n", - "\n", - " self.conc_mass_comp = Var(\n", - " self.params.solute_set,\n", - " domain=NonNegativeReals,\n", - " initialize={\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1},\n", - " doc=\"Component mass concentrations\",\n", - " units=units.g / units.L,\n", - " )\n", - "\n", - " self.pressure = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=1,\n", - " bounds=(1, 5),\n", - " units=units.atm,\n", - " doc=\"State pressure [atm]\",\n", - " )\n", - "\n", - " self.temperature = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=300,\n", - " bounds=(273, 373),\n", - " units=units.K,\n", - " doc=\"State temperature [K]\",\n", - " )\n", - "\n", - " def material_flow_expression(self, j):\n", - " if j == \"H2O\":\n", - " return self.flow_vol * self.params.dens_mass\n", - " else:\n", - " return self.conc_mass_comp[j] * self.flow_vol\n", - "\n", - " self.material_flow_expression = Expression(\n", - " self.component_list,\n", - " rule=material_flow_expression,\n", - " doc=\"Material flow terms\",\n", - " )\n", - "\n", - " def enthalpy_flow_expression(self):\n", - " return (\n", - " self.flow_vol\n", - " * self.params.dens_mass\n", - " * self.params.cp_mass\n", - " * (self.temperature - self.params.temperature_ref)\n", - " )\n", - "\n", - " self.enthalpy_flow_expression = Expression(\n", - " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", - " )\n", - "\n", - " def get_flow_rate(self):\n", - " return self.flow_vol\n", - "\n", - " def get_material_flow_terms(self, p, j):\n", - " return self.material_flow_expression[j]\n", - "\n", - " def get_enthalpy_flow_terms(self, p):\n", - " return self.enthalpy_flow_expression\n", - "\n", - " def default_material_balance_type(self):\n", - " return MaterialBalanceType.componentTotal\n", - "\n", - " def default_energy_balance_type(self):\n", - " return EnergyBalanceType.enthalpyTotal\n", - "\n", - " def define_state_vars(self):\n", - " return {\n", - " \"flow_vol\": self.flow_vol,\n", - " \"conc_mass_comp\": self.conc_mass_comp,\n", - " \"temperature\": self.temperature,\n", - " \"pressure\": self.pressure,\n", - " }\n", - "\n", - " def get_material_flow_basis(self):\n", - " return MaterialFlowBasis.mass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 3. Liquid Liquid Extractor Unit Model\n", - "\n", - "Following the creation of property packages, our next step is to develop a unit model that facilitates the mass transfer of solutes between phases. This involves importing necessary libraries, building the unit model, defining auxiliary functions, and establishing the initialization routine for the unit model.\n", - "\n", - "## 3.1 Importing necessary libraries\n", - "\n", - "Let's commence by importing the essential libraries from Pyomo and IDAES." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "# Import Pyomo libraries\n", - "from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool\n", - "from pyomo.environ import (\n", - " value,\n", - " Constraint,\n", - " check_optimal_termination,\n", - ")\n", - "\n", - "# Import IDAES cores\n", - "from idaes.core import (\n", - " ControlVolume0DBlock,\n", - " declare_process_block_class,\n", - " MaterialBalanceType,\n", - " EnergyBalanceType,\n", - " MaterialFlowBasis,\n", - " MomentumBalanceType,\n", - " UnitModelBlockData,\n", - " useDefault,\n", - ")\n", - "from idaes.core.util.config import (\n", - " is_physical_parameter_block,\n", - " is_reaction_parameter_block,\n", - ")\n", - "\n", - "import idaes.logger as idaeslog\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.util.exceptions import ConfigurationError, InitializationError" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3.2 Creating the unit model\n", - "\n", - "Creating a unit model starts by creating a class called `LiqExtractionData` and use the `declare_process_block_class` decorator. The `LiqExtractionData` inherits the properties of `UnitModelBlockData` class, which allows us to create a control volume which is necessary for the unit model. After declaration of the class we proceed to define the relevant config arguments for the control volume. The config arguments includes the following properties:\n", - "\n", - "- `material_balance_type` - Indicates what type of mass balance should be constructed\n", - "- `has_pressure_change` - Indicates whether terms for pressure change should be\n", - "constructed\n", - "- `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", - "constructed\n", - "- `Organic Property` - Property parameter object used to define property calculations\n", - "for the Organic phase\n", - "- `Organic Property Arguments` - Arguments to use for constructing Organic phase properties\n", - "- `Aqueous Property` - Property parameter object used to define property calculations\n", - "for the aqueous phase\n", - "- `Aqueous Property Arguments` - Arguments to use for constructing aqueous phase properties\n", - "\n", - "As there are no pressure changes or reactions in this scenario, configuration arguments for these aspects are not included. However, additional details on configuration arguments can be found [here](https://github.com/IDAES/idaes-pse/blob/8948c6ce27d4c7f2c06b377a173f413599091998/idaes/models/unit_models/cstr.py)." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "@declare_process_block_class(\"LiqExtraction\")\n", - "class LiqExtractionData(UnitModelBlockData):\n", - " \"\"\"\n", - " LiqExtraction Unit Model Class\n", - " \"\"\"\n", - "\n", - " CONFIG = UnitModelBlockData.CONFIG()\n", - "\n", - " CONFIG.declare(\n", - " \"material_balance_type\",\n", - " ConfigValue(\n", - " default=MaterialBalanceType.useDefault,\n", - " domain=In(MaterialBalanceType),\n", - " description=\"Material balance construction flag\",\n", - " doc=\"\"\"Indicates what type of mass balance should be constructed,\n", - " **default** - MaterialBalanceType.useDefault.\n", - " **Valid values:** {\n", - " **MaterialBalanceType.useDefault - refer to property package for default\n", - " balance type\n", - " **MaterialBalanceType.none** - exclude material balances,\n", - " **MaterialBalanceType.componentPhase** - use phase component balances,\n", - " **MaterialBalanceType.componentTotal** - use total component balances,\n", - " **MaterialBalanceType.elementTotal** - use total element balances,\n", - " **MaterialBalanceType.total** - use total material balance.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_pressure_change\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Pressure change term construction flag\",\n", - " doc=\"\"\"Indicates whether terms for pressure change should be\n", - " constructed,\n", - " **default** - False.\n", - " **Valid values:** {\n", - " **True** - include pressure change terms,\n", - " **False** - exclude pressure change terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_phase_equilibrium\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Phase equilibrium construction flag\",\n", - " doc=\"\"\"Indicates whether terms for phase equilibrium should be\n", - " constructed,\n", - " **default** = False.\n", - " **Valid values:** {\n", - " **True** - include phase equilibrium terms\n", - " **False** - exclude phase equilibrium terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"organic_property_package\",\n", - " ConfigValue(\n", - " default=useDefault,\n", - " domain=is_physical_parameter_block,\n", - " description=\"Property package to use for organic phase\",\n", - " doc=\"\"\"Property parameter object used to define property calculations\n", - " for the organic phase,\n", - " **default** - useDefault.\n", - " **Valid values:** {\n", - " **useDefault** - use default package from parent model or flowsheet,\n", - " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"organic_property_package_args\",\n", - " ConfigBlock(\n", - " implicit=True,\n", - " description=\"Arguments to use for constructing organic phase properties\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to organic phase\n", - " property block(s) and used when constructing these,\n", - " **default** - None.\n", - " **Valid values:** {\n", - " see property package for documentation.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"aqueous_property_package\",\n", - " ConfigValue(\n", - " default=useDefault,\n", - " domain=is_physical_parameter_block,\n", - " description=\"Property package to use for aqueous phase\",\n", - " doc=\"\"\"Property parameter object used to define property calculations\n", - " for the aqueous phase,\n", - " **default** - useDefault.\n", - " **Valid values:** {\n", - " **useDefault** - use default package from parent model or flowsheet,\n", - " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"aqueous_property_package_args\",\n", - " ConfigBlock(\n", - " implicit=True,\n", - " description=\"Arguments to use for constructing aqueous phase properties\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to aqueous phase\n", - " property block(s) and used when constructing these,\n", - " **default** - None.\n", - " **Valid values:** {\n", - " see property package for documentation.}\"\"\",\n", - " ),\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Building the model\n", - "\n", - "After constructing the `LiqExtractionData` block and defining the config arguments for the control block, the next step is to write a build function that incorporates control volume and establishes constraints on the control volume to achieve the desired mass transfer. The control volume serves as a pivotal component in the unit model construction, representing the volume in which the process unfolds.\n", - "\n", - "IDAES provides flexibility in choosing control volumes based on geometry, with options including 0D or 1D. In this instance, we opt for a 0D control volume, the most commonly used control volume. This choice is suitable for systems where there is a well-mixed volume of fluid or where spatial variations are deemed negligible.\n", - "\n", - "The control volume encompasses parameters from (1-8), and its equations are configured to satisfy the specified config arguments. For a more in-depth understanding, users are encouraged to refer to [this resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst). \n", - "\n", - "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Org' for the Organic phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", - "\n", - "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the Organic phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, and the property package, along with property package arguments. \n", - "\n", - "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the Organic property package\n", - "\n", - "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", - "\n", - "$\\frac{\\partial M_{t, p, j}}{\\partial t} = F_{in, t, p, j} - F_{out, t, p, j} + N_{kinetic, t, p, j} + N_{equilibrium, t, p, j} + N_{pe, t, p, j} + N_{transfer, t, p, j} + N_{custom, t, p, j}$\n", - "\n", - "- $\\frac{\\partial M_{t, p, j}}{\\partial t}$ - Material accumulation\n", - "- $F_{in, t, p, j}$ - Flow into the control volume\n", - "- $F_{out, t, p, j}$ - Flow out of the control volume\n", - "- $N_{kinetic, t, p, j}$ - Rate of reaction generation\n", - "- $N_{equilibrium, t, p, j}$ - Equilibrium reaction generation\n", - "- $N_{pe, t, p, j}$ - Equilibrium reaction extent\n", - "- $N_{transfer, t, p, j}$ - Mass transfer\n", - "- $N_{custom, t, p, j}$ - User defined terms in material balance\n", - "\n", - "- t indicates time index\n", - "- p indicates phase index\n", - "- j indicates component index\n", - "- e indicates element index\n", - "- r indicates reaction name index\n", - "\n", - "Here we shall see that $N_{transfer, t, p, j}$ is the term in the equation which is responsible for the mass transfer and the `mass_transfer_term` should only be equal to the amount being transferred and not include a material balance on our own. For a detailed description of the terms one should refer to the following [resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst)\n", - "\n", - "This concludes the creation of organic phase control volume. Similar procedure is done for the aqueous phase control volume with aqueous property package. \n", - "\n", - "Now, the unit model has two control volumes with appropriate configurations and material, momentum and energy balances. The next step is to check the basis of the two property packages. They should both have the same flow basis, and an error is raised if this is not the case.\n", - "\n", - "Following this, the `add_inlet_ports` and `add_outlet_ports` functions are used to create inlet and outlet ports. These ports are named and assigned to each control volume, resulting in labeled inlet and outlet ports for each control volume.\n", - "\n", - "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the organic phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{org} = - mass\\_transfer\\_term_{aq} $\n", - "\n", - "Here $mass\\_transfer\\_term_{p}$ is the term indicating the amount of material being transferred from/to the phase and $D_{i}$ is the Distribution co-efficient for component i. \n", - "\n", - "This marks the completion of the build function, and the unit model is now equipped with the necessary process constraints. The subsequent steps involve writing the initialization routine." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "def build(self):\n", - " \"\"\"\n", - " Begin building model (pre-DAE transformation).\n", - " Args:\n", - " None\n", - " Returns:\n", - " None\n", - " \"\"\"\n", - " # Call UnitModel.build to setup dynamics\n", - " super().build()\n", - "\n", - " # Check phase lists match assumptions\n", - " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the aquoues \"\n", - " f\"phase property package have a single phase named 'Aq'\"\n", - " )\n", - " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", - " f\"phase property package have a single phase named 'Org'\"\n", - " )\n", - "\n", - " # Check for at least one common component in component lists\n", - " if not any(\n", - " j in self.config.aqueous_property_package.component_list\n", - " for j in self.config.organic_property_package.component_list\n", - " ):\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", - " f\"and aqueous phase property packages have at least one \"\n", - " f\"common component.\"\n", - " )\n", - "\n", - " self.organic_phase = ControlVolume0DBlock(\n", - " dynamic=self.config.dynamic,\n", - " property_package=self.config.organic_property_package,\n", - " property_package_args=self.config.organic_property_package_args,\n", - " )\n", - "\n", - " self.organic_phase.add_state_blocks(\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium\n", - " )\n", - "\n", - " # Separate organic and aqueous phases means that phase equilibrium will\n", - " # be handled at the unit model level, thus has_phase_equilibrium is\n", - " # False, but has_mass_transfer is True.\n", - "\n", - " self.organic_phase.add_material_balances(\n", - " balance_type=self.config.material_balance_type,\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", - " has_mass_transfer=True,\n", - " )\n", - " # ---------------------------------------------------------------------\n", - "\n", - " self.aqueous_phase = ControlVolume0DBlock(\n", - " dynamic=self.config.dynamic,\n", - " property_package=self.config.aqueous_property_package,\n", - " property_package_args=self.config.aqueous_property_package_args,\n", - " )\n", - "\n", - " self.aqueous_phase.add_state_blocks(\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium\n", - " )\n", - "\n", - " # Separate liquid and aqueous phases means that phase equilibrium will\n", - " # be handled at the unit model level, thus has_phase_equilibrium is\n", - " # False, but has_mass_transfer is True.\n", - "\n", - " self.aqueous_phase.add_material_balances(\n", - " balance_type=self.config.material_balance_type,\n", - " # has_rate_reactions=False,\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", - " has_mass_transfer=True,\n", - " )\n", - "\n", - " self.aqueous_phase.add_geometry()\n", - "\n", - " # ---------------------------------------------------------------------\n", - " # Check flow basis is compatible\n", - " t_init = self.flowsheet().time.first()\n", - " if (\n", - " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", - " ):\n", - " raise ConfigurationError(\n", - " f\"{self.name} aqueous and organic property packages must use the \"\n", - " f\"same material flow basis.\"\n", - " )\n", - "\n", - " self.organic_phase.add_geometry()\n", - "\n", - " # Add Ports\n", - " self.add_inlet_port(\n", - " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", - " )\n", - " self.add_inlet_port(\n", - " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", - " )\n", - " self.add_outlet_port(\n", - " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", - " )\n", - " self.add_outlet_port(\n", - " name=\"aqueous_outlet\",\n", - " block=self.aqueous_phase,\n", - " doc=\"Aqueous outlet\",\n", - " )\n", - "\n", - " # ---------------------------------------------------------------------\n", - " # Add unit level constraints\n", - " # First, need the union and intersection of component lists\n", - " all_comps = (\n", - " self.aqueous_phase.properties_out.component_list\n", - " | self.organic_phase.properties_out.component_list\n", - " )\n", - " common_comps = (\n", - " self.aqueous_phase.properties_out.component_list\n", - " & self.organic_phase.properties_out.component_list\n", - " )\n", - "\n", - " # Get units for unit conversion\n", - " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", - " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", - " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - "\n", - " if flow_basis == MaterialFlowBasis.mass:\n", - " fb = \"flow_mass\"\n", - " elif flow_basis == MaterialFlowBasis.molar:\n", - " fb = \"flow_mole\"\n", - " else:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor only supports mass \"\n", - " f\"basis for MaterialFlowBasis.\"\n", - " )\n", - "\n", - " # Material balances\n", - " def rule_material_aq_balance(self, t, j):\n", - " if j in common_comps:\n", - " return self.aqueous_phase.mass_transfer_term[\n", - " t, \"Aq\", j\n", - " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", - " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", - " )\n", - " elif j in self.organic_phase.properties_out.component_list:\n", - " # No mass transfer term\n", - " # Set organic flowrate to an arbitrary small value\n", - " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", - " elif j in self.aqueous_phase.properties_out.component_list:\n", - " # No mass transfer term\n", - " # Set aqueous flowrate to an arbitrary small value\n", - " return self.aqueous_phase.mass_transfer_term[t, \"Aq\", j] == 0 * aunits(fb)\n", - "\n", - " self.material_aq_balance = Constraint(\n", - " self.flowsheet().time,\n", - " self.aqueous_phase.properties_out.component_list,\n", - " rule=rule_material_aq_balance,\n", - " doc=\"Unit level material balances for Aq\",\n", - " )\n", - "\n", - " def rule_material_liq_balance(self, t, j):\n", - " if j in common_comps:\n", - " return (\n", - " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", - " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", - " )\n", - " else:\n", - " # No mass transfer term\n", - " # Set organic flowrate to an arbitrary small value\n", - " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", - "\n", - " self.material_org_balance = Constraint(\n", - " self.flowsheet().time,\n", - " self.organic_phase.properties_out.component_list,\n", - " rule=rule_material_liq_balance,\n", - " doc=\"Unit level material balances Org\",\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Initialization Routine\n", - "\n", - "After writing the unit model it is crucial to initialize the model properly, as non-linear models may encounter local minima or infeasibility if not initialized properly. IDAES provides us with a few initialization routines which may not work for all the models, and in such cases the developer will have to define their own initialization routines. \n", - "\n", - "To create a custom initialization routine, model developers must create an initialize method as part of their model, and provide a sequence of steps intended to build up a feasible solution. Initialization routines generally make use of Pyomo’s tools for activating and deactivating constraints and often involve solving multiple sub-problems whilst building up an initial state.\n", - "\n", - "For this tutorial we would use the pre-defined initialization routine of `BlockTriangularizationInitializer` when initializing the model in the flowsheet. This Initializer should be suitable for most models, but may struggle to initialize\n", - "tightly coupled systems of equations. This method of initialization will follow the following workflow. \n", - "\n", - "- Have precheck for structural singularity\n", - "- Run incidence analysis on given block data and check matching.\n", - "- Call Block Triangularization solver on model.\n", - "- Call solve_strongly_connected_components on a given BlockData.\n", - "\n", - "For more details about this initialization routine can be found [here](https://github.com/IDAES/idaes-pse/blob/c09433b9afed5ae2fe25c0ccdc732783324f0101/idaes/core/initialization/block_triangularization.py). \n", - "\n", - "\n", - "This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_doc.md). The next sections will deal with the diagonistics and testing of the property package and unit model. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3.3 Building a Flowsheet\n", - "\n", - "Once we have set up the unit model and its property packages, we can start building a flowsheet using them. In this tutorial, we're focusing on a simple flowsheet with just a liquid-liquid extractor. To create the flowsheet we follow the following steps:\n", - "\n", - "- Import necessary libraries\n", - "- Create a Pyomo model.\n", - "- Inside the model, create a flowsheet block.\n", - "- Assign property packages to the flowsheet block.\n", - "- Add the liquid-liquid extractor to the flowsheet block.\n", - "- Fix variable to make it a square problem\n", - "- Run an initialization process.\n", - "- Solve the flowsheet.\n", - "\n", - "Following these steps, we've built a basic flowsheet using Pyomo. For more details, refer to the [documentation](../../flowsheets/hda_flowsheet_with_distillation_doc.md).\n" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "import pyomo.environ as pyo\n", - "import idaes.core\n", - "import idaes.models.unit_models\n", - "from idaes.core.solvers import get_solver\n", - "import idaes.logger as idaeslog\n", - "from pyomo.network import Arc\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.initialization import InitializationStatus\n", - "from idaes.core.initialization.block_triangularization import (\n", - " BlockTriangularizationInitializer,\n", - ")\n", - "from liquid_extraction.organic_property import OrgPhase\n", - "from liquid_extraction.aqueous_property import AqPhase\n", - "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", - "\n", - "\n", - "def build_model():\n", - " m = pyo.ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.lex = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - " return m\n", - "\n", - "\n", - "def fix_state_variables(m):\n", - " m.fs.lex.organic_inlet.flow_vol.fix(80 * pyo.units.L / pyo.units.hour)\n", - " m.fs.lex.organic_inlet.temperature.fix(300 * pyo.units.K)\n", - " m.fs.lex.organic_inlet.pressure.fix(1 * pyo.units.atm)\n", - " m.fs.lex.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", - " 1e-5 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", - " 1e-5 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", - " 1e-5 * pyo.units.g / pyo.units.L\n", - " )\n", - "\n", - " m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", - " m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", - " m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", - " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", - " 0.15 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", - " 0.2 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", - " 0.1 * pyo.units.g / pyo.units.L\n", - " )\n", - "\n", - " return m\n", - "\n", - "\n", - "def initialize_model(m):\n", - " initializer = BlockTriangularizationInitializer()\n", - " initializer.initialize(m.fs.lex)\n", - " return m\n", - "\n", - "\n", - "def main():\n", - " m = build_model()\n", - " m = fix_state_variables(m)\n", - " m = initialize_model(m)\n", - " return m\n", - "\n", - "\n", - "if __name__ == main:\n", - " main()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 4. Model Diagnostics using DiagnosticsToolbox\n", - "\n", - "Here, during initialization, we encounter warnings indicating that variables are being set to negative values, which is not expected behavior. These warnings suggest that there may be flaws in the model that require further investigation using the DiagnosticsToolbox from IDAES. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter_doc.md).\n", - "\n", - "To proceed with investigating these issues, we need to import the DiagnosticsToolbox. We can gain a better understanding of its functionality by running the help function on it. " - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "from idaes.core.util import DiagnosticsToolbox" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The help() function provides comprehensive information on the DiagnosticsToolbox and all its supported methods. However, it's essential to focus on the initial steps outlined at the beginning of the docstring to get started effectively.\n", - "\n", - "Here's a breakdown of the steps to start with:\n", - "\n", - "- `Instantiate Model:` Ensure you have an instance of the model with a degrees of freedom equal to 0.\n", - "\n", - "- `Create DiagnosticsToolbox Instance:` Next, instantiate a DiagnosticsToolbox object.\n", - "\n", - "- `Provide Model to DiagnosticsToolbox:` Pass the model instance to the DiagnosticsToolbox.\n", - "\n", - "- `Call report_structural_issues() Function:` Finally, call the report_structural_issues() function. This function will highlight any warnings in the model's structure, such as unit inconsistencies or other issues related to variables in the caution section.\n", - "\n", - "By following these steps, you can efficiently utilize the DiagnosticsToolbox to identify and address any structural issues or warnings in your model." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "WARNING (W1001): Setting Var\n", - "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", - "`-0.1725` (float) not in domain NonNegativeReals.\n", - " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", - "WARNING (W1001): Setting Var\n", - "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", - "`-0.4` (float) not in domain NonNegativeReals.\n", - " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", - "WARNING (W1001): Setting Var\n", - "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", - "`-0.05` (float) not in domain NonNegativeReals.\n", - " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", - "====================================================================================\n", - "Model Statistics\n", - "\n", - " Activated Blocks: 21 (Deactivated: 0)\n", - " Free Variables in Activated Constraints: 16 (External: 0)\n", - " Free Variables with only lower bounds: 8\n", - " Free Variables with only upper bounds: 0\n", - " Free Variables with upper and lower bounds: 0\n", - " Fixed Variables in Activated Constraints: 8 (External: 0)\n", - " Activated Equality Constraints: 16 (Deactivated: 0)\n", - " Activated Inequality Constraints: 0 (Deactivated: 0)\n", - " Activated Objectives: 0 (Deactivated: 0)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "0 WARNINGS\n", - "\n", - " No warnings found!\n", - "\n", - "------------------------------------------------------------------------------------\n", - "1 Cautions\n", - "\n", - " Caution: 10 unused variables (4 fixed)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "Suggested next steps:\n", - "\n", - " Try to initialize/solve your model and then call report_numerical_issues()\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "m = main()\n", - "dt = DiagnosticsToolbox(m)\n", - "dt.report_structural_issues()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Although no warnings were reported, it's important to note that there are 3 variables fixed to 0 and 10 unused variables, out of which 4 are fixed. As indicated in the output, the next step is to solve the model. After solving, you should call the report_numerical_issues() function. This function will help identify any numerical issues that may arise during the solution process." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "###############################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "###############################################################################" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "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...: 33\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 14\n", - "\n", - "Total number of variables............................: 16\n", - " variables with only lower bounds: 8\n", - " variables with lower and upper bounds: 0\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 16\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 4.00e+01 4.93e+01 -1.0 4.10e-01 - 9.91e-01 2.41e-02h 1\n", - " 2 0.0000000e+00 4.00e+01 2.03e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", - " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", - " 4r 0.0000000e+00 4.00e+01 9.88e+04 1.6 3.68e+02 - 9.92e-01 2.29e-03f 1\n", - " 5r 0.0000000e+00 3.60e+01 3.03e+00 1.6 4.01e+00 - 1.00e+00 1.00e+00f 1\n", - " 6r 0.0000000e+00 3.69e+01 1.21e+01 -1.2 9.24e-01 - 9.69e-01 9.78e-01f 1\n", - " 7r 0.0000000e+00 3.70e+01 2.11e-01 -1.9 1.00e-01 - 9.97e-01 1.00e+00f 1\n", - " 8r 0.0000000e+00 3.78e+01 2.03e-02 -4.3 8.71e-01 - 9.71e-01 1.00e+00f 1\n", - " 9r 0.0000000e+00 3.80e+01 2.62e-04 -6.4 1.24e-01 - 9.99e-01 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 3.81e+01 5.87e-09 -6.4 1.58e-01 - 1.00e+00 1.00e+00f 1\n", - " 11r 0.0000000e+00 3.91e+01 1.09e-05 -9.0 9.35e-01 - 9.68e-01 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 11\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 5.1393961893966849e-07 5.1393961893966849e-07\n", - "Constraint violation....: 3.9105165554489545e+01 3.9105165554489545e+01\n", - "Complementarity.........: 9.0909090910996620e-10 9.0909090910996620e-10\n", - "Overall NLP error.......: 3.9105165554489545e+01 3.9105165554489545e+01\n", - "\n", - "\n", - "Number of objective function evaluations = 17\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 17\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 14\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 12\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", - "WARNING: Loading a SolverResults object with a warning status into\n", - "model.name=\"unknown\";\n", - " - termination condition: infeasible\n", - " - message from solver: Ipopt 3.13.2\\x3a Converged to a locally infeasible\n", - " point. Problem may be infeasible.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating Custom Unit Model\n", + "Author: Javal Vyas \n", + "Maintainer: Javal Vyas \n", + "\n", + "This tutorial is a comprehensive step-wise procedure to build a custom unit model from scratch. This tutorial will include creating a property package, a custom unit model and testing them. For this tutorial we shall create a custom unit model for Liquid - Liquid Extraction. \n", + "\n", + "The Liquid - Liquid Extractor model contains two immiscible fluids forming the two phases. One of the phases, say phase_1 has a high concentration of solutes which is to be separated. A mass transfer happens between the two phases and the solute is transferred from phase_1 to phase_2. This mass transfer is governed by a parameter called the distribution coefficient.\n", + "\n", + "After reviewing the working principles of the Liquid - Liquid Extractor, we shall proceed to create a custom unit model. We will require a property package for each phase, a custom unit model class and tests for the model and property packages.\n", + "\n", + "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", + "- Steady-state only\n", + "- Organic phase property package has a single phase named Org\n", + "- Aqueous phase property package has a single phase named Aq\n", + "- Organic and Aqueous phase properties need not have the same component list. \n", + "\n", + "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " + ] }, { - "data": { - "text/plain": [ - "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06552338600158691}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1. Creating Organic Property Package\n", + "\n", + "Creating a property package is a 4 step process\n", + "- Import necessary libraries \n", + "- Creating Physical Parameter Data Block\n", + "- Define State Block\n", + "- Define State Block Data\n", + "\n", + "# 1.1 Importing necessary packages \n", + "Let us begin with importing the necessary libraries where we will be using functionalities from IDAES and Pyomo. " ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "solver = pyo.SolverFactory(\"ipopt\")\n", - "solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The model is probably infeasible thus indicating numerical issues with the model. We should call the `report_numerical_issues()` function and check what the constraints/variables causing this issue. " - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "====================================================================================\n", - "Model Statistics\n", - "\n", - " Jacobian Condition Number: 7.955E+03\n", - "\n", - "------------------------------------------------------------------------------------\n", - "2 WARNINGS\n", - "\n", - " WARNING: 6 Constraints with large residuals (>1.0E-05)\n", - " WARNING: 5 Variables at or outside bounds (tol=0.0E+00)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "3 Cautions\n", - "\n", - " Caution: 8 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", - " Caution: 5 Variables with value close to zero (tol=1.0E-08)\n", - " Caution: 3 Variables with extreme value (<1.0E-04 or >1.0E+04)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "Suggested next steps:\n", - "\n", - " display_constraints_with_large_residuals()\n", - " display_variables_at_or_outside_bounds()\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "dt.report_numerical_issues()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this scenario, it's observed that the condition number of the Jacobian is high, indicating that the Jacobian is ill-conditioned. Additionally, there are 2 warnings related to constraints with large residuals and variables at or outside the bounds. The cautions mentioned in the output are also related to these warnings.\n", - "\n", - "As suggested, the next steps would be to:\n", - "\n", - "- Call the `display_variables_at_or_outside_bounds()` function to investigate variables at or outside the bounds.\n", - "\n", - "- Call the `display_constraints_with_large_residuals()` function to examine constraints with large residuals.\n", - "\n", - "These steps will help identify the underlying causes of the numerical issues and constraints violations, allowing for further analysis and potential resolution. " - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Python libraries\n", + "import logging\n", + "\n", + "import idaes.logger as idaeslog\n", + "from idaes.core.util.initialization import fix_state_vars\n", + "\n", + "# Import Pyomo libraries\n", + "from pyomo.environ import (\n", + " Param,\n", + " Set,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", + "\n", + "# Import IDAES cores\n", + "from idaes.core import (\n", + " declare_process_block_class,\n", + " MaterialFlowBasis,\n", + " PhysicalParameterBlock,\n", + " StateBlockData,\n", + " StateBlock,\n", + " MaterialBalanceType,\n", + " EnergyBalanceType,\n", + " Solute,\n", + " Solvent,\n", + " LiquidPhase,\n", + ")\n", + "from idaes.core.util.model_statistics import degrees_of_freedom" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "====================================================================================\n", - "The following variable(s) have values at or outside their bounds (tol=0.0E+00):\n", - "\n", - " fs.lex.organic_phase.properties_in[0.0].pressure (fixed): value=1.0 bounds=(1, 5)\n", - " fs.lex.organic_phase.properties_out[0.0].pressure (free): value=1 bounds=(1, 5)\n", - " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl] (free): value=0.0 bounds=(0, None)\n", - " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3] (free): value=0.0 bounds=(0, None)\n", - " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4] (free): value=0.0 bounds=(0, None)\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "dt.display_variables_at_or_outside_bounds()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this scenario, there are a couple of issues to address:\n", - "\n", - "- The pressure variable is fixed to 1, which is its lower bound. This could potentially lead to numerical issues, although it may not affect the model significantly since there is no pressure change in the model. To mitigate this, consider adjusting the lower bound of the pressure variable to avoid having its value at or outside the bounds.\n", - "\n", - "- The more concerning issue is with the `conc_mass_comp` variable attempting to go below 0 in the output. This suggests that there may be constraints involving `conc_mass_comp` in the aqueous phase causing this behavior. To investigate further, it's recommended to call the `display_constraints_with_large_residuals()` function. This will provide insights into whether constraints involving `conc_mass_comp` are contributing to the convergence issue." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1.2 Physical Parameter Data Block\n", + "\n", + "A `PhysicalParameterBlock` serves as the central point of reference for all aspects of the property package and needs to define several things about the package. These are summarized below:\n", + "\n", + "- Units of measurement\n", + "- What properties are supported and how they are implemented\n", + "- What components and phases are included in the packages\n", + "- All the global parameters necessary for calculating properties\n", + "- A reference to the associated State Block class, so that construction of the State Block components can be automated from the Physical Parameter Block\n", + "\n", + "To construct this block, we begin by declaring a process block class using a Python decorator. One can learn more about `declare_process_block_class` [here](https://github.com/IDAES/idaes-pse/blob/eea1209077b75f7d940d8958362e69d4650c079d/idaes/core/base/process_block.py#L173). After constructing the process block, we define a build function which contains all the components that the property package would have. `super` function here is used to give access to methods and properties of a parent or sibling class and since this is used on the class `PhysicalParameterData` class, build has access to all the parent and sibling class methods. \n", + "\n", + "The `PhysicalParameterBlock` then refers to the `state block`, in this case `OrgPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. The solvent is in the Organic phase; we will assign the Phase as OrganicPhase, and the variable will be named Org as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", + " \n", + "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the Organic phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. Additionally, a parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", + "\n", + "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). In this method, we will call the predefined method `add_default_units()`.\n", + "\n", + "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default. " + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "====================================================================================\n", - "The following constraint(s) have large residuals (>1.0E-05):\n", - "\n", - " fs.lex.material_aq_balance[0.0,NaCl]: 5.49716E-01\n", - " fs.lex.material_aq_balance[0.0,KNO3]: 8.94833E-01\n", - " fs.lex.material_aq_balance[0.0,CaSO4]: 5.48843E-02\n", - " fs.lex.aqueous_phase.material_balances[0.0,NaCl]: 1.67003E+01\n", - " fs.lex.aqueous_phase.material_balances[0.0,KNO3]: 3.91052E+01\n", - " fs.lex.aqueous_phase.material_balances[0.0,CaSO4]: 4.94512E+00\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "dt.display_constraints_with_large_residuals()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As expected there are convergence issues with the constraints which have `conc_mass_comp` variable in them specifically in the aqeous phase. Now, let us investigate further by printing this constraints and checking the value of each term. Since this is an persistent issue across the components, we can focus on just one of the component to identify the issue. " - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "@declare_process_block_class(\"OrgPhase\")\n", + "class PhysicalParameterData(PhysicalParameterBlock):\n", + " \"\"\"\n", + " Property Parameter Block Class\n", + "\n", + " Contains parameters and indexing sets associated with properties for\n", + " organic Phase\n", + "\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction.\n", + " \"\"\"\n", + " super().build()\n", + "\n", + " self._state_block_class = OrgPhaseStateBlock\n", + "\n", + " # List of valid phases in property package\n", + " self.Org = LiquidPhase()\n", + "\n", + " # Component list - a list of component identifiers\n", + " self.NaCl = Solute()\n", + " self.KNO3 = Solute()\n", + " self.CaSO4 = Solute()\n", + " self.solvent = (\n", + " Solvent()\n", + " ) # Solvent used here is ethylene dibromide (Organic Polar)\n", + "\n", + " # Heat capacity of solvent\n", + " self.cp_mass = Param(\n", + " mutable=True,\n", + " initialize=717.01,\n", + " doc=\"Specific heat capacity of solvent\",\n", + " units=units.J / units.kg / units.K,\n", + " )\n", + "\n", + " self.dens_mass = Param(\n", + " mutable=True,\n", + " initialize=2170,\n", + " doc=\"Density of ethylene dibromide\",\n", + " units=units.kg / units.m**3,\n", + " )\n", + " self.temperature_ref = Param(\n", + " within=PositiveReals,\n", + " mutable=True,\n", + " default=298.15,\n", + " doc=\"Reference temperature\",\n", + " units=units.K,\n", + " )\n", + " self.diffusion_factor = Param(\n", + " self.solute_set,\n", + " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", + " within=PositiveReals,\n", + " mutable=True,\n", + " )\n", + "\n", + " @classmethod\n", + " def define_metadata(cls, obj):\n", + " obj.add_default_units(\n", + " {\n", + " \"time\": units.hour,\n", + " \"length\": units.m,\n", + " \"mass\": units.g,\n", + " \"amount\": units.mol,\n", + " \"temperature\": units.K,\n", + " }\n", + " )" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{Member of material_balances} : Material balances\n", - " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", - " Key : Lower : Body : Upper : Active\n", - " (0.0, 'NaCl') : 0.0 : (fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) - (fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_out[0.0].flow_vol) + fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] : 0.0 : True\n" - ] - } - ], - "source": [ - "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1.3 State Block\n", + "\n", + "After the `PhysicalParameterBlock` class has been created, the next step is to write the code necessary to create the State Blocks that will be used throughout the flowsheet. `StateBlock` contains all the information necessary to define the state of the system. This includes the state variables and constraints on those variables which are used to describe a state property like the enthalpy, material balance, etc.\n", + "\n", + "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed StateBlock all at once (rather than element by element).\n", + "\n", + "The class `_OrganicStateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the `fix_initialization_states` function. `fix_initialization_states` function is used to fix the state variable within the state block with the provided initial values (usually inlet conditions). It takes a `block` as the argument in which the state variables are to be fixed. It also takes `state_args` as an optional argument. `state_args` is a dictionary with the value for the state variables to be fixed. This function returns a dictionary indexed by the block, state variables and variable index indicating the fixed status of each variable before applying the function. \n", + "\n", + "The above function comprise of the _OrganicStateBlock. Next, we shall see the construction of the OrgPhaseStateBlockData class." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{Member of conc_mass_comp} : Component mass concentrations\n", - " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " NaCl : 0 : 0.15 : None : True : True : NonNegativeReals\n", - "flow_vol : Total volumetric flowrate\n", - " Size=1, Index=None, Units=l/h\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " None : 0 : 100.0 : None : True : True : NonNegativeReals\n", - "{Member of conc_mass_comp} : Component mass concentrations\n", - " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " NaCl : 0 : 0.0 : None : False : False : NonNegativeReals\n", - "flow_vol : Total volumetric flowrate\n", - " Size=1, Index=None, Units=l/h\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " None : 0 : 100.0 : None : False : False : NonNegativeReals\n", - "{Member of mass_transfer_term} : Component material transfer into unit\n", - " Size=4, Index=fs._time*fs.aq_properties._phase_component_set, Units=g/h\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " (0.0, 'Aq', 'NaCl') : None : -31.700284300098897 : None : False : False : Reals\n" - ] - } - ], - "source": [ - "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", - "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", - "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", - "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", - "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It seems there is a discrepancy between the mass transfer term and the amount of input of NaCl. This can be inferred from the values where the input equals 15g/h and the `mass_transfer_term` equals -31.706g/h.\n", - "\n", - "To further investigate this issue, it's advisable to examine the `material_aq_balance` constraint within the unit model where the `mass_transfer_term` is defined. By printing out this constraint and analyzing its components, you can gain a better understanding of the discrepancy and take appropriate corrective actions." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "class _OrganicStateBlock(StateBlock):\n", + " \"\"\"\n", + " This Class contains methods which should be applied to Property Blocks as a\n", + " whole, rather than individual elements of indexed Property Blocks.\n", + " \"\"\"\n", + "\n", + " def fix_initialization_states(self):\n", + " fix_state_vars(self)" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{Member of material_aq_balance} : Unit level material balances for Aq\n", - " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", - " Key : Lower : Body : Upper : Active\n", - " (0.0, 'NaCl') : 0.0 : fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] + fs.org_properties.diffusion_factor[NaCl]*(fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) : 0.0 : True\n" - ] - } - ], - "source": [ - "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here the problem can be tracked down easily as there being a typing error while recording the distribution factor. The distribution factor here was wrongly written ignoring its magnitude which should have been 1e-2, but that was missed, thus adjusting the distribution factor parameter we should have this issue resolved. " - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", - ")\n", - "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", - ")\n", - "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", - ")\n", - "\n", - "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", - "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "After the corrective actions, we should check if this have made any structural issues, for this we would call `report_structural_issues()`" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The class `OrgPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `OrgPhaseStateBlock`, and inherits the block class from `_OrganicStateBlock`. This inheritance allows `OrgPhaseStateBlockData` to leverage functions from `_OrganicStateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", + "\n", + "The subsequent objective is to delineate the state variables, accomplished through the `_make_state_vars` method. This method encompasses all the essential state variables and associated data. For this particular property package, the required state variables are:\n", + "\n", + "- `flow_vol` - volumetric flow rate\n", + "- `conc_mass_comp` - mass fractions\n", + "- `pressure` - state pressure\n", + "- `temperature` - state temperature\n", + "\n", + "After establishing the state variables, the subsequent step involves setting up state properties as constraints. This includes specifying the relationships and limitations that dictate the system's behavior. The following properties need to be articulated:\n", + "\n", + "-`get_material_flow_terms`: quantifies the amount of material flow.\n", + "- `get_enthalpy_flow_terms`: quantifies the amount of enthalpy flow.\n", + "- `get_flow_rate`: details volumetric flow rates.\n", + "- `default_material_balance_type`: defines the kind of material balance to be used.\n", + "- `default_energy_balance_type`: defines the kind of energy balance to be used.\n", + "- `define_state_vars`: involves defining state variables with units, akin to the define_metadata function in the PhysicalParameterData block.\n", + "- `get_material_flow_basis`: establishes the basis on which state variables are measured, whether in mass or molar terms.\n", + "\n", + "These definitions mark the conclusion of the state block construction and thus the property package. For additional details on creating a property package, please refer to this [resource](../../properties/custom/custom_physical_property_packages_doc.md ).\n", + "\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "====================================================================================\n", - "Model Statistics\n", - "\n", - " Activated Blocks: 21 (Deactivated: 0)\n", - " Free Variables in Activated Constraints: 16 (External: 0)\n", - " Free Variables with only lower bounds: 8\n", - " Free Variables with only upper bounds: 0\n", - " Free Variables with upper and lower bounds: 0\n", - " Fixed Variables in Activated Constraints: 8 (External: 0)\n", - " Activated Equality Constraints: 16 (Deactivated: 0)\n", - " Activated Inequality Constraints: 0 (Deactivated: 0)\n", - " Activated Objectives: 0 (Deactivated: 0)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "0 WARNINGS\n", - "\n", - " No warnings found!\n", - "\n", - "------------------------------------------------------------------------------------\n", - "1 Cautions\n", - "\n", - " Caution: 10 unused variables (4 fixed)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "Suggested next steps:\n", - "\n", - " Try to initialize/solve your model and then call report_numerical_issues()\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "dt.report_structural_issues()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now since there are no warnings we can go ahead and solve the model and see if the results are optimal. " - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_OrganicStateBlock)\n", + "class OrgPhaseStateBlockData(StateBlockData):\n", + " \"\"\"\n", + " An example property package for Organic phase for liquid liquid extraction\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction\n", + " \"\"\"\n", + " super().build()\n", + " self._make_state_vars()\n", + "\n", + " def _make_state_vars(self):\n", + " self.flow_vol = Var(\n", + " initialize=1,\n", + " domain=NonNegativeReals,\n", + " doc=\"Total volumetric flowrate\",\n", + " units=units.L / units.hour,\n", + " )\n", + " self.conc_mass_comp = Var(\n", + " self.params.solute_set,\n", + " domain=NonNegativeReals,\n", + " initialize=1,\n", + " doc=\"Component mass concentrations\",\n", + " units=units.g / units.L,\n", + " )\n", + " self.pressure = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=1,\n", + " bounds=(1, 5),\n", + " units=units.atm,\n", + " doc=\"State pressure [atm]\",\n", + " )\n", + "\n", + " self.temperature = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=300,\n", + " bounds=(273, 373),\n", + " units=units.K,\n", + " doc=\"State temperature [K]\",\n", + " )\n", + "\n", + " def material_flow_expression(self, j):\n", + " if j == \"solvent\":\n", + " return self.flow_vol * self.params.dens_mass\n", + " else:\n", + " return self.flow_vol * self.conc_mass_comp[j]\n", + "\n", + " self.material_flow_expression = Expression(\n", + " self.component_list,\n", + " rule=material_flow_expression,\n", + " doc=\"Material flow terms\",\n", + " )\n", + "\n", + " def enthalpy_flow_expression(self):\n", + " return (\n", + " self.flow_vol\n", + " * self.params.dens_mass\n", + " * self.params.cp_mass\n", + " * (self.temperature - self.params.temperature_ref)\n", + " )\n", + "\n", + " self.enthalpy_flow_expression = Expression(\n", + " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", + " )\n", + "\n", + " def get_flow_rate(self):\n", + " return self.flow_vol\n", + "\n", + " def get_material_flow_terms(self, p, j):\n", + " return self.material_flow_expression[j]\n", + "\n", + " def get_enthalpy_flow_terms(self, p):\n", + " return self.enthalpy_flow_expression\n", + "\n", + " def default_material_balance_type(self):\n", + " return MaterialBalanceType.componentTotal\n", + "\n", + " def default_energy_balance_type(self):\n", + " return EnergyBalanceType.enthalpyTotal\n", + "\n", + " def define_state_vars(self):\n", + " return {\n", + " \"flow_vol\": self.flow_vol,\n", + " \"conc_mass_comp\": self.conc_mass_comp,\n", + " \"temperature\": self.temperature,\n", + " \"pressure\": self.pressure,\n", + " }\n", + "\n", + " def get_material_flow_basis(self):\n", + " return MaterialFlowBasis.mass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 2. Creating Aqueous Property Package\n", + "\n", + "The structure of the Aqueous Property Package mirrors that of the Organic Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Python libraries\n", + "import logging\n", + "\n", + "from idaes.core.util.initialization import fix_state_vars\n", + "\n", + "# Import Pyomo libraries\n", + "from pyomo.environ import (\n", + " Param,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", + "\n", + "# Import IDAES cores\n", + "from idaes.core import (\n", + " declare_process_block_class,\n", + " MaterialFlowBasis,\n", + " PhysicalParameterBlock,\n", + " StateBlockData,\n", + " StateBlock,\n", + " MaterialBalanceType,\n", + " EnergyBalanceType,\n", + " Solute,\n", + " Solvent,\n", + " LiquidPhase,\n", + ")\n", + "\n", + "# Some more information about this module\n", + "__author__ = \"Javal Vyas\"\n", + "\n", + "\n", + "# Set up logger\n", + "_log = logging.getLogger(__name__)\n", + "\n", + "\n", + "@declare_process_block_class(\"AqPhase\")\n", + "class AqPhaseData(PhysicalParameterBlock):\n", + " \"\"\"\n", + " Property Parameter Block Class\n", + "\n", + " Contains parameters and indexing sets associated with properties for\n", + " aqueous Phase\n", + "\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction.\n", + " \"\"\"\n", + " super().build()\n", + "\n", + " self._state_block_class = AqPhaseStateBlock\n", + "\n", + " # List of valid phases in property package\n", + " self.Aq = LiquidPhase()\n", + "\n", + " # Component list - a list of component identifiers\n", + " self.NaCl = Solute()\n", + " self.KNO3 = Solute()\n", + " self.CaSO4 = Solute()\n", + " self.H2O = Solvent()\n", + "\n", + " # Heat capacity of solvent\n", + " self.cp_mass = Param(\n", + " mutable=True,\n", + " initialize=4182,\n", + " doc=\"Specific heat capacity of solvent\",\n", + " units=units.J / units.kg / units.K,\n", + " )\n", + "\n", + " self.dens_mass = Param(\n", + " mutable=True,\n", + " initialize=997,\n", + " doc=\"Density of ethylene dibromide\",\n", + " units=units.kg / units.m**3,\n", + " )\n", + " self.temperature_ref = Param(\n", + " within=PositiveReals,\n", + " mutable=True,\n", + " default=298.15,\n", + " doc=\"Reference temperature\",\n", + " units=units.K,\n", + " )\n", + "\n", + " @classmethod\n", + " def define_metadata(cls, obj):\n", + " obj.add_default_units(\n", + " {\n", + " \"time\": units.hour,\n", + " \"length\": units.m,\n", + " \"mass\": units.g,\n", + " \"amount\": units.mol,\n", + " \"temperature\": units.K,\n", + " }\n", + " )\n", + "\n", + "\n", + "class _AqueousStateBlock(StateBlock):\n", + " \"\"\"\n", + " This Class contains methods which should be applied to Property Blocks as a\n", + " whole, rather than individual elements of indexed Property Blocks.\n", + " \"\"\"\n", + "\n", + " def fix_initialization_states(self):\n", + " fix_state_vars(self)\n", + "\n", + "\n", + "@declare_process_block_class(\"AqPhaseStateBlock\", block_class=_AqueousStateBlock)\n", + "class AqPhaseStateBlockData(StateBlockData):\n", + " \"\"\"\n", + " An example property package for ideal gas properties with Gibbs energy\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction\n", + " \"\"\"\n", + " super().build()\n", + " self._make_state_vars()\n", + "\n", + " def _make_state_vars(self):\n", + " self.flow_vol = Var(\n", + " initialize=1,\n", + " domain=NonNegativeReals,\n", + " doc=\"Total volumetric flowrate\",\n", + " units=units.L / units.hour,\n", + " )\n", + "\n", + " self.conc_mass_comp = Var(\n", + " self.params.solute_set,\n", + " domain=NonNegativeReals,\n", + " initialize={\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1},\n", + " doc=\"Component mass concentrations\",\n", + " units=units.g / units.L,\n", + " )\n", + "\n", + " self.pressure = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=1,\n", + " bounds=(1, 5),\n", + " units=units.atm,\n", + " doc=\"State pressure [atm]\",\n", + " )\n", + "\n", + " self.temperature = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=300,\n", + " bounds=(273, 373),\n", + " units=units.K,\n", + " doc=\"State temperature [K]\",\n", + " )\n", + "\n", + " def material_flow_expression(self, j):\n", + " if j == \"H2O\":\n", + " return self.flow_vol * self.params.dens_mass\n", + " else:\n", + " return self.conc_mass_comp[j] * self.flow_vol\n", + "\n", + " self.material_flow_expression = Expression(\n", + " self.component_list,\n", + " rule=material_flow_expression,\n", + " doc=\"Material flow terms\",\n", + " )\n", + "\n", + " def enthalpy_flow_expression(self):\n", + " return (\n", + " self.flow_vol\n", + " * self.params.dens_mass\n", + " * self.params.cp_mass\n", + " * (self.temperature - self.params.temperature_ref)\n", + " )\n", + "\n", + " self.enthalpy_flow_expression = Expression(\n", + " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", + " )\n", + "\n", + " def get_flow_rate(self):\n", + " return self.flow_vol\n", + "\n", + " def get_material_flow_terms(self, p, j):\n", + " return self.material_flow_expression[j]\n", + "\n", + " def get_enthalpy_flow_terms(self, p):\n", + " return self.enthalpy_flow_expression\n", + "\n", + " def default_material_balance_type(self):\n", + " return MaterialBalanceType.componentTotal\n", + "\n", + " def default_energy_balance_type(self):\n", + " return EnergyBalanceType.enthalpyTotal\n", + "\n", + " def define_state_vars(self):\n", + " return {\n", + " \"flow_vol\": self.flow_vol,\n", + " \"conc_mass_comp\": self.conc_mass_comp,\n", + " \"temperature\": self.temperature,\n", + " \"pressure\": self.pressure,\n", + " }\n", + "\n", + " def get_material_flow_basis(self):\n", + " return MaterialFlowBasis.mass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 3. Liquid Liquid Extractor Unit Model\n", + "\n", + "Following the creation of property packages, our next step is to develop a unit model that facilitates the mass transfer of solutes between phases. This involves importing necessary libraries, building the unit model, defining auxiliary functions, and establishing the initialization routine for the unit model.\n", + "\n", + "## 3.1 Importing necessary libraries\n", + "\n", + "Let's commence by importing the essential libraries from Pyomo and IDAES." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Pyomo libraries\n", + "from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool\n", + "from pyomo.environ import (\n", + " value,\n", + " Constraint,\n", + " check_optimal_termination,\n", + ")\n", + "\n", + "# Import IDAES cores\n", + "from idaes.core import (\n", + " ControlVolume0DBlock,\n", + " declare_process_block_class,\n", + " MaterialBalanceType,\n", + " EnergyBalanceType,\n", + " MaterialFlowBasis,\n", + " MomentumBalanceType,\n", + " UnitModelBlockData,\n", + " useDefault,\n", + ")\n", + "from idaes.core.util.config import (\n", + " is_physical_parameter_block,\n", + " is_reaction_parameter_block,\n", + ")\n", + "\n", + "import idaes.logger as idaeslog\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.util.exceptions import ConfigurationError, InitializationError" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3.2 Creating the unit model\n", + "\n", + "Creating a unit model starts by creating a class called `LiqExtractionData` and using the `declare_process_block_class` decorator. The `LiqExtractionData` inherits the properties of the `UnitModelBlockData` class, which allows us to create a control volume that is necessary for the unit model. After declaration of the class we proceed to define the relevant config arguments for the control volume. The config arguments include the following properties:\n", + "\n", + "- `material_balance_type` - Indicates what type of mass balance should be constructed\n", + "- `has_pressure_change` - Indicates whether terms for pressure change should be\n", + "constructed\n", + "- `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", + "constructed\n", + "- `organic_property_package` - Property parameter object used to define property calculations\n", + "for the Organic phase\n", + "- `organic_property_package_args` - Arguments to use for constructing Organic phase properties\n", + "- `aqueous_property_package` - Property parameter object used to define property calculations\n", + "for the aqueous phase\n", + "- `aqueous_property_package_args` - Arguments to use for constructing aqueous phase properties\n", + "\n", + "As there are no pressure changes or reactions in this scenario, configuration arguments for these aspects are not included. However, additional details on configuration arguments can be found [here](https://github.com/IDAES/idaes-pse/blob/8948c6ce27d4c7f2c06b377a173f413599091998/idaes/models/unit_models/cstr.py)." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "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...: 33\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 14\n", - "\n", - "Total number of variables............................: 16\n", - " variables with only lower bounds: 8\n", - " variables with lower and upper bounds: 0\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 16\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 5.85e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 3.55e-15 8.41e+00 -1.0 5.85e+01 - 1.05e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 1\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 3.5527136788005009e-15 3.5527136788005009e-15\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 3.5527136788005009e-15 3.5527136788005009e-15\n", - "\n", - "\n", - "Number of objective function evaluations = 2\n", - "Number of objective gradient evaluations = 2\n", - "Number of equality constraint evaluations = 2\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 2\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 1\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" - ] + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "@declare_process_block_class(\"LiqExtraction\")\n", + "class LiqExtractionData(UnitModelBlockData):\n", + " \"\"\"\n", + " LiqExtraction Unit Model Class\n", + " \"\"\"\n", + "\n", + " CONFIG = UnitModelBlockData.CONFIG()\n", + "\n", + " CONFIG.declare(\n", + " \"material_balance_type\",\n", + " ConfigValue(\n", + " default=MaterialBalanceType.useDefault,\n", + " domain=In(MaterialBalanceType),\n", + " description=\"Material balance construction flag\",\n", + " doc=\"\"\"Indicates what type of mass balance should be constructed,\n", + " **default** - MaterialBalanceType.useDefault.\n", + " **Valid values:** {\n", + " **MaterialBalanceType.useDefault - refer to property package for default\n", + " balance type\n", + " **MaterialBalanceType.none** - exclude material balances,\n", + " **MaterialBalanceType.componentPhase** - use phase component balances,\n", + " **MaterialBalanceType.componentTotal** - use total component balances,\n", + " **MaterialBalanceType.elementTotal** - use total element balances,\n", + " **MaterialBalanceType.total** - use total material balance.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"has_pressure_change\",\n", + " ConfigValue(\n", + " default=False,\n", + " domain=Bool,\n", + " description=\"Pressure change term construction flag\",\n", + " doc=\"\"\"Indicates whether terms for pressure change should be\n", + " constructed,\n", + " **default** - False.\n", + " **Valid values:** {\n", + " **True** - include pressure change terms,\n", + " **False** - exclude pressure change terms.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"has_phase_equilibrium\",\n", + " ConfigValue(\n", + " default=False,\n", + " domain=Bool,\n", + " description=\"Phase equilibrium construction flag\",\n", + " doc=\"\"\"Indicates whether terms for phase equilibrium should be\n", + " constructed,\n", + " **default** = False.\n", + " **Valid values:** {\n", + " **True** - include phase equilibrium terms\n", + " **False** - exclude phase equilibrium terms.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"organic_property_package\",\n", + " ConfigValue(\n", + " default=useDefault,\n", + " domain=is_physical_parameter_block,\n", + " description=\"Property package to use for organic phase\",\n", + " doc=\"\"\"Property parameter object used to define property calculations\n", + " for the organic phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"organic_property_package_args\",\n", + " ConfigBlock(\n", + " implicit=True,\n", + " description=\"Arguments to use for constructing organic phase properties\",\n", + " doc=\"\"\"A ConfigBlock with arguments to be passed to organic phase\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"aqueous_property_package\",\n", + " ConfigValue(\n", + " default=useDefault,\n", + " domain=is_physical_parameter_block,\n", + " description=\"Property package to use for aqueous phase\",\n", + " doc=\"\"\"Property parameter object used to define property calculations\n", + " for the aqueous phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"aqueous_property_package_args\",\n", + " ConfigBlock(\n", + " implicit=True,\n", + " description=\"Arguments to use for constructing aqueous phase properties\",\n", + " doc=\"\"\"A ConfigBlock with arguments to be passed to aqueous phase\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", + " ),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Building the model\n", + "\n", + "After constructing the `LiqExtractionData` block and defining the config arguments for the control block, the next step is to write a build function that incorporates the control volume and establishes constraints on the control volume to achieve the desired mass transfer. The control volume serves as a pivotal component in the unit model construction, representing the volume in which the process unfolds.\n", + "\n", + "IDAES provides flexibility in choosing control volumes based on geometry, with options including 0D or 1D. In this instance, we opt for a 0D control volume, the most commonly used control volume. This choice is suitable for systems where there is a well-mixed volume of fluid or where spatial variations are deemed negligible.\n", + "\n", + "The control volume encompasses parameters from (1-8), and its equations are configured to satisfy the specified config arguments. For a more in-depth understanding, users are encouraged to refer to [this resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst). \n", + "\n", + "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Org' for the organic phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", + "\n", + "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the organic phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, and the property package, along with property package arguments. \n", + "\n", + "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the organic property package\n", + "\n", + "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", + "\n", + "$\\frac{\\partial M_{t, p, j}}{\\partial t} = F_{in, t, p, j} - F_{out, t, p, j} + N_{kinetic, t, p, j} + N_{equilibrium, t, p, j} + N_{pe, t, p, j} + N_{transfer, t, p, j} + N_{custom, t, p, j}$\n", + "\n", + "- $\\frac{\\partial M_{t, p, j}}{\\partial t}$ - Material accumulation\n", + "- $F_{in, t, p, j}$ - Flow into the control volume\n", + "- $F_{out, t, p, j}$ - Flow out of the control volume\n", + "- $N_{kinetic, t, p, j}$ - Rate of reaction generation\n", + "- $N_{equilibrium, t, p, j}$ - Equilibrium reaction generation\n", + "- $N_{pe, t, p, j}$ - Equilibrium reaction extent\n", + "- $N_{transfer, t, p, j}$ - Mass transfer\n", + "- $N_{custom, t, p, j}$ - User defined terms in material balance\n", + "\n", + "- t indicates time index\n", + "- p indicates phase index\n", + "- j indicates component index\n", + "- e indicates element index\n", + "- r indicates reaction name index\n", + "\n", + "Here we shall see that $N_{transfer, t, p, j}$ is the term in the equation which is responsible for the mass transfer and the `mass_transfer_term` should only be equal to the amount being transferred and not include a material balance on our own. For a detailed description of the terms one should refer to the following [resource.](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst)\n", + "\n", + "This concludes the creation of the organic phase control volume. A similar procedure is done for the aqueous phase control volume with aqueous property package. \n", + "\n", + "Now, the unit model has two control volumes with appropriate configurations and material, momentum and energy balances. The next step is to check the basis of the two property packages. They should both have the same flow basis, and an error is raised if this is not the case.\n", + "\n", + "Following this, the `add_inlet_ports` and `add_outlet_ports` functions are used to create inlet and outlet ports. These ports are named and assigned to each control volume, resulting in labeled inlet and outlet ports for each control volume.\n", + "\n", + "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the organic phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{org} = - mass\\_transfer\\_term_{aq} $\n", + "\n", + "Here $mass\\_transfer\\_term_{p}$ is the term indicating the amount of material being transferred from/to the phase and $D_{i}$ is the Distribution coefficient for component i. \n", + "\n", + "This marks the completion of the build function, and the unit model is now equipped with the necessary process constraints. The subsequent steps involve writing the initialization routine." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "def build(self):\n", + " \"\"\"\n", + " Begin building model (pre-DAE transformation).\n", + " Args:\n", + " None\n", + " Returns:\n", + " None\n", + " \"\"\"\n", + " # Call UnitModel.build to setup dynamics\n", + " super().build()\n", + "\n", + " # Check phase lists match assumptions\n", + " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the aqueous \"\n", + " f\"phase property package have a single phase named 'Aq'\"\n", + " )\n", + " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"phase property package have a single phase named 'Org'\"\n", + " )\n", + "\n", + " # Check for at least one common component in component lists\n", + " if not any(\n", + " j in self.config.aqueous_property_package.component_list\n", + " for j in self.config.organic_property_package.component_list\n", + " ):\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"and aqueous phase property packages have at least one \"\n", + " f\"common component.\"\n", + " )\n", + "\n", + " self.organic_phase = ControlVolume0DBlock(\n", + " dynamic=self.config.dynamic,\n", + " property_package=self.config.organic_property_package,\n", + " property_package_args=self.config.organic_property_package_args,\n", + " )\n", + "\n", + " self.organic_phase.add_state_blocks(\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium\n", + " )\n", + "\n", + " # Separate organic and aqueous phases means that phase equilibrium will\n", + " # be handled at the unit model level, thus has_phase_equilibrium is\n", + " # False, but has_mass_transfer is True.\n", + "\n", + " self.organic_phase.add_material_balances(\n", + " balance_type=self.config.material_balance_type,\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", + " has_mass_transfer=True,\n", + " )\n", + " # ---------------------------------------------------------------------\n", + "\n", + " self.aqueous_phase = ControlVolume0DBlock(\n", + " dynamic=self.config.dynamic,\n", + " property_package=self.config.aqueous_property_package,\n", + " property_package_args=self.config.aqueous_property_package_args,\n", + " )\n", + "\n", + " self.aqueous_phase.add_state_blocks(\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium\n", + " )\n", + "\n", + " # Separate liquid and aqueous phases means that phase equilibrium will\n", + " # be handled at the unit model level, thus has_phase_equilibrium is\n", + " # False, but has_mass_transfer is True.\n", + "\n", + " self.aqueous_phase.add_material_balances(\n", + " balance_type=self.config.material_balance_type,\n", + " # has_rate_reactions=False,\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", + " has_mass_transfer=True,\n", + " )\n", + "\n", + " self.aqueous_phase.add_geometry()\n", + "\n", + " # ---------------------------------------------------------------------\n", + " # Check flow basis is compatible\n", + " t_init = self.flowsheet().time.first()\n", + " if (\n", + " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", + " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", + " ):\n", + " raise ConfigurationError(\n", + " f\"{self.name} aqueous and organic property packages must use the \"\n", + " f\"same material flow basis.\"\n", + " )\n", + "\n", + " self.organic_phase.add_geometry()\n", + "\n", + " # Add Ports\n", + " self.add_inlet_port(\n", + " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", + " )\n", + " self.add_inlet_port(\n", + " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", + " )\n", + " self.add_outlet_port(\n", + " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", + " )\n", + " self.add_outlet_port(\n", + " name=\"aqueous_outlet\",\n", + " block=self.aqueous_phase,\n", + " doc=\"Aqueous outlet\",\n", + " )\n", + "\n", + " # ---------------------------------------------------------------------\n", + " # Add unit level constraints\n", + " # First, need the union and intersection of component lists\n", + " all_comps = (\n", + " self.aqueous_phase.properties_out.component_list\n", + " | self.organic_phase.properties_out.component_list\n", + " )\n", + " common_comps = (\n", + " self.aqueous_phase.properties_out.component_list\n", + " & self.organic_phase.properties_out.component_list\n", + " )\n", + "\n", + " # Get units for unit conversion\n", + " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", + " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", + " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", + "\n", + " if flow_basis == MaterialFlowBasis.mass:\n", + " fb = \"flow_mass\"\n", + " else:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor only supports mass \"\n", + " f\"basis for MaterialFlowBasis.\"\n", + " )\n", + "\n", + " # Material balances\n", + " def rule_material_aq_balance(self, t, j):\n", + " if j in common_comps:\n", + " return self.aqueous_phase.mass_transfer_term[\n", + " t, \"Aq\", j\n", + " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", + " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", + " )\n", + " elif j in self.organic_phase.properties_out.component_list:\n", + " # No mass transfer term\n", + " # Set organic flowrate to an arbitrary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", + " elif j in self.aqueous_phase.properties_out.component_list:\n", + " # No mass transfer term\n", + " # Set aqueous flowrate to an arbitrary small value\n", + " return self.aqueous_phase.mass_transfer_term[t, \"Aq\", j] == 0 * aunits(fb)\n", + "\n", + " self.material_aq_balance = Constraint(\n", + " self.flowsheet().time,\n", + " self.aqueous_phase.properties_out.component_list,\n", + " rule=rule_material_aq_balance,\n", + " doc=\"Unit level material balances for Aq\",\n", + " )\n", + "\n", + " def rule_material_liq_balance(self, t, j):\n", + " if j in common_comps:\n", + " return (\n", + " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", + " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", + " )\n", + " else:\n", + " # No mass transfer term\n", + " # Set organic flowrate to an arbitrary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", + "\n", + " self.material_org_balance = Constraint(\n", + " self.flowsheet().time,\n", + " self.organic_phase.properties_out.component_list,\n", + " rule=rule_material_liq_balance,\n", + " doc=\"Unit level material balances Org\",\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialization Routine\n", + "\n", + "After writing the unit model it is crucial to initialize the model properly, as non-linear models may encounter local minima or infeasibility if not initialized properly. IDAES provides us with a few initialization routines which may not work for all the models, and in such cases the developer will have to define their own initialization routines. \n", + "\n", + "To create a custom initialization routine, model developers must create an initialize method as part of their model, and provide a sequence of steps intended to build up a feasible solution. Initialization routines generally make use of Pyomo\u2019s tools for activating and deactivating constraints and often involve solving multiple sub-problems whilst building up an initial state.\n", + "\n", + "For this tutorial we would use the pre-defined initialization routine of `BlockTriangularizationInitializer` when initializing the model in the flowsheet. This Initializer should be suitable for most models, but may struggle to initialize\n", + "tightly coupled systems of equations. This method of initialization will follow the following workflow. \n", + "\n", + "- Have precheck for structural singularity\n", + "- Run incidence analysis on given block data and check matching.\n", + "- Call Block Triangularization solver on the model.\n", + "- Call solve_strongly_connected_components on a given BlockData.\n", + "\n", + "More details about this initialization routine can be found [here](https://github.com/IDAES/idaes-pse/blob/c09433b9afed5ae2fe25c0ccdc732783324f0101/idaes/core/initialization/block_triangularization.py). \n", + "\n", + "\n", + "This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_doc.md). The next sections will deal with the diagnostics and testing of the property package and unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3.3 Building a Flowsheet\n", + "\n", + "Once we have set up the unit model and its property packages, we can start building a flowsheet using them. In this tutorial, we're focusing on a simple flowsheet with just a liquid-liquid extractor. To create the flowsheet we follow the following steps:\n", + "\n", + "- Import necessary libraries\n", + "- Create a Pyomo model.\n", + "- Inside the model, create a flowsheet block.\n", + "- Assign property packages to the flowsheet block.\n", + "- Add the liquid-liquid extractor to the flowsheet block.\n", + "- Fix variable to make it a square problem\n", + "- Run an initialization process.\n", + "- Solve the flowsheet.\n", + "\n", + "Following these steps, we've built a basic flowsheet using Pyomo. For more details, refer to the [documentation](../../flowsheets/hda_flowsheet_with_distillation_doc.md).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "Ipopt 3.13.2: linear_solver=\"ma57\"\n", + "max_iter=200\n", + "nlp_scaling_method=\"gradient-based\"\n", + "tol=1e-08\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...: 6\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 0\n", + "\n", + "Total number of variables............................: 6\n", + " variables with only lower bounds: 6\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.98e+01 -1.0 4.10e-01 - 1.00e+00 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.05e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.97e+04 1.6 3.67e+02 - 1.00e+00 2.30e-03f 1\n", + " 5r 0.0000000e+00 4.00e+01 1.13e+02 1.6 3.89e-01 - 9.97e-01 1.00e+00f 1\n", + " 6r 0.0000000e+00 4.00e+01 1.13e+00 -1.2 8.42e-02 - 9.92e-01 9.87e-01f 1\n", + " 7r 0.0000000e+00 4.00e+01 9.78e-04 -4.3 8.94e-04 - 1.00e+00 1.00e+00f 1\n", + " 8r 0.0000000e+00 4.00e+01 5.79e-05 -6.4 1.49e-07 - 1.00e+00 1.00e+00f 1\n", + " 9r 0.0000000e+00 4.00e+01 5.20e-06 -9.0 6.12e-10 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 9\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 2.5739427655935287e-07 2.5739427655935287e-07\n", + "Constraint violation....: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "Complementarity.........: 9.0909090910996662e-10 9.0909090910996662e-10\n", + "Overall NLP error.......: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 15\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 15\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 12\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "Could not solve fs.lex after block triangularization finished.\n" + ] + } + ], + "source": [ + "import pyomo.environ as pyo\n", + "from idaes.core import FlowsheetBlock\n", + "\n", + "from idaes.core.initialization.block_triangularization import (\n", + " BlockTriangularizationInitializer,\n", + ")\n", + "from liquid_extraction.organic_property import OrgPhase\n", + "from liquid_extraction.aqueous_property import AqPhase\n", + "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", + "\n", + "\n", + "def build_model():\n", + " m = pyo.ConcreteModel()\n", + " m.fs = FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.lex = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + " return m\n", + "\n", + "\n", + "def fix_state_variables(m):\n", + " m.fs.lex.organic_inlet.flow_vol.fix(80 * pyo.units.L / pyo.units.hour)\n", + " m.fs.lex.organic_inlet.temperature.fix(300 * pyo.units.K)\n", + " m.fs.lex.organic_inlet.pressure.fix(1 * pyo.units.atm)\n", + " m.fs.lex.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", + " 1e-5 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", + " 1e-5 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", + " 1e-5 * pyo.units.g / pyo.units.L\n", + " )\n", + "\n", + " m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", + " m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", + " m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", + " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", + " 0.15 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", + " 0.2 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", + " 0.1 * pyo.units.g / pyo.units.L\n", + " )\n", + "\n", + " return m\n", + "\n", + "\n", + "def initialize_model(m):\n", + " initializer = BlockTriangularizationInitializer()\n", + " try:\n", + " initializer.initialize(m.fs.lex)\n", + " except InitializationError as err:\n", + " print(err)\n", + " return m\n", + "\n", + "\n", + "def main():\n", + " m = build_model()\n", + " m = fix_state_variables(m)\n", + " m = initialize_model(m)\n", + " return m\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4. Model Diagnostics using DiagnosticsToolbox\n", + "\n", + "Here, during initialization, we encounter warnings indicating that variables are being set to negative values before an exception is raised stating that solving the model failed. These issues suggest that there may be flaws in the model that require further investigation using the DiagnosticsToolbox from IDAES. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter_doc.md).\n", + "\n", + "To proceed with investigating these issues, we need to import the DiagnosticsToolbox. We can gain a better understanding of its functionality by running the help function on it. " + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "from idaes.core.util import DiagnosticsToolbox" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The help() function provides comprehensive information on the DiagnosticsToolbox and all its supported methods. However, it's essential to focus on the initial steps outlined at the beginning of the docstring to get started effectively.\n", + "\n", + "Here's a breakdown of the steps to start with:\n", + "\n", + "- `Instantiate Model:` Ensure you have an instance of the model with degrees of freedom equal to 0.\n", + "\n", + "- `Create DiagnosticsToolbox Instance:` Next, instantiate a DiagnosticsToolbox object.\n", + "\n", + "- `Provide Model to DiagnosticsToolbox:` Pass the model instance to the DiagnosticsToolbox.\n", + "\n", + "- `Call report_structural_issues() Function:` Finally, call the report_structural_issues() function. This function will highlight any warnings in the model's structure, such as unit inconsistencies or other issues related to variables in the caution section.\n", + "\n", + "By following these steps, you can efficiently utilize the DiagnosticsToolbox to identify and address any structural issues or warnings in your model." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "Ipopt 3.13.2: linear_solver=\"ma57\"\n", + "max_iter=200\n", + "nlp_scaling_method=\"gradient-based\"\n", + "tol=1e-08\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...: 6\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 0\n", + "\n", + "Total number of variables............................: 6\n", + " variables with only lower bounds: 6\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.98e+01 -1.0 4.10e-01 - 1.00e+00 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.05e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.97e+04 1.6 3.67e+02 - 1.00e+00 2.30e-03f 1\n", + " 5r 0.0000000e+00 4.00e+01 1.13e+02 1.6 3.89e-01 - 9.97e-01 1.00e+00f 1\n", + " 6r 0.0000000e+00 4.00e+01 1.13e+00 -1.2 8.42e-02 - 9.92e-01 9.87e-01f 1\n", + " 7r 0.0000000e+00 4.00e+01 9.78e-04 -4.3 8.94e-04 - 1.00e+00 1.00e+00f 1\n", + " 8r 0.0000000e+00 4.00e+01 5.79e-05 -6.4 1.49e-07 - 1.00e+00 1.00e+00f 1\n", + " 9r 0.0000000e+00 4.00e+01 5.20e-06 -9.0 6.12e-10 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 9\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 2.5739427655935287e-07 2.5739427655935287e-07\n", + "Constraint violation....: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "Complementarity.........: 9.0909090910996662e-10 9.0909090910996662e-10\n", + "Overall NLP error.......: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 15\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 15\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 12\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "Could not solve fs.lex after block triangularization finished.\n", + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "1 Cautions\n", + "\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "m = main()\n", + "dt = DiagnosticsToolbox(m)\n", + "dt.report_structural_issues()" + ] }, { - "data": { - "text/plain": [ - "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.07779264450073242}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Although no warnings were reported, it's important to note that there are 3 variables fixed to 0 and 10 unused variables, out of which 4 are fixed. As indicated in the output, the next step is to solve the model. After solving, you should call the report_numerical_issues() function. This function will help identify any numerical issues that may arise during the solution process." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "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...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.93e+01 -1.0 4.10e-01 - 9.91e-01 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.03e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.88e+04 1.6 3.68e+02 - 9.92e-01 2.29e-03f 1\n", + " 5r 0.0000000e+00 3.60e+01 3.03e+00 1.6 4.01e+00 - 1.00e+00 1.00e+00f 1\n", + " 6r 0.0000000e+00 3.69e+01 1.21e+01 -1.2 9.24e-01 - 9.69e-01 9.78e-01f 1\n", + " 7r 0.0000000e+00 3.70e+01 2.11e-01 -1.9 1.00e-01 - 9.97e-01 1.00e+00f 1\n", + " 8r 0.0000000e+00 3.78e+01 2.03e-02 -4.3 8.71e-01 - 9.71e-01 1.00e+00f 1\n", + " 9r 0.0000000e+00 3.80e+01 2.62e-04 -6.4 1.24e-01 - 9.99e-01 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10r 0.0000000e+00 3.81e+01 5.87e-09 -6.4 1.58e-01 - 1.00e+00 1.00e+00f 1\n", + " 11r 0.0000000e+00 3.91e+01 1.09e-05 -9.0 9.35e-01 - 9.68e-01 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 11\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 5.1393961893966849e-07 5.1393961893966849e-07\n", + "Constraint violation....: 3.9105165554489545e+01 3.9105165554489545e+01\n", + "Complementarity.........: 9.0909090910996620e-10 9.0909090910996620e-10\n", + "Overall NLP error.......: 3.9105165554489545e+01 3.9105165554489545e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 17\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 17\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 14\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "WARNING: Loading a SolverResults object with a warning status into\n", + "model.name=\"unknown\";\n", + " - termination condition: infeasible\n", + " - message from solver: Ipopt 3.13.2\\x3a Converged to a locally infeasible\n", + " point. Problem may be infeasible.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06122612953186035}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver = pyo.SolverFactory(\"ipopt\")\n", + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model is probably infeasible, indicating numerical issues with the model. We should call the `report_numerical_issues()` function and check the constraints/variables causing this issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Jacobian Condition Number: 7.955E+03\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 WARNINGS\n", + "\n", + " WARNING: 6 Constraints with large residuals (>1.0E-05)\n", + " WARNING: 5 Variables at or outside bounds (tol=0.0E+00)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "3 Cautions\n", + "\n", + " Caution: 8 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", + " Caution: 5 Variables with value close to zero (tol=1.0E-08)\n", + " Caution: 3 Variables with extreme value (<1.0E-04 or >1.0E+04)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " display_constraints_with_large_residuals()\n", + " compute_infeasibility_explanation()\n", + " display_variables_at_or_outside_bounds()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_numerical_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, it's observed that the condition number of the Jacobian is high, indicating that the Jacobian is ill-conditioned. Additionally, there are 2 warnings related to constraints with large residuals and variables at or outside the bounds. The cautions mentioned in the output are also related to these warnings.\n", + "\n", + "As suggested, the next steps would be to:\n", + "\n", + "- Call the `display_variables_at_or_outside_bounds()` function to investigate variables at or outside the bounds.\n", + "\n", + "- Call the `display_constraints_with_large_residuals()` function to examine constraints with large residuals.\n", + "\n", + "These steps will help identify the underlying causes of the numerical issues and constraints violations, allowing for further analysis and potential resolution. " + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following variable(s) have values at or outside their bounds (tol=0.0E+00):\n", + "\n", + " fs.lex.organic_phase.properties_in[0.0].pressure (fixed): value=1.0 bounds=(1, 5)\n", + " fs.lex.organic_phase.properties_out[0.0].pressure (free): value=1 bounds=(1, 5)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4] (free): value=0.0 bounds=(0, None)\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_variables_at_or_outside_bounds()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, there are a couple of issues to address:\n", + "\n", + "- The pressure variable is fixed to 1, which is its lower bound. This could potentially lead to numerical issues, although it may not affect the model significantly since there is no pressure change in the model. To mitigate this, consider adjusting the lower bound of the pressure variable to avoid having its value at or outside the bounds.\n", + "\n", + "- The more concerning issue is with the `conc_mass_comp` variable attempting to go below 0 in the output. This suggests that there may be constraints involving `conc_mass_comp` in the aqueous phase causing this behavior. To investigate further, it's recommended to call the `display_constraints_with_large_residuals()` function. This will provide insights into whether constraints involving `conc_mass_comp` are contributing to the convergence issue." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following constraint(s) have large residuals (>1.0E-05):\n", + "\n", + " fs.lex.material_aq_balance[0.0,NaCl]: 5.49716E-01\n", + " fs.lex.material_aq_balance[0.0,KNO3]: 8.94833E-01\n", + " fs.lex.material_aq_balance[0.0,CaSO4]: 5.48843E-02\n", + " fs.lex.aqueous_phase.material_balances[0.0,NaCl]: 1.67003E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,KNO3]: 3.91052E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,CaSO4]: 4.94512E+00\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_constraints_with_large_residuals()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected there are convergence issues with the constraints which have `conc_mass_comp` variable in them specifically in the aqueous phase. Now, let us investigate further by printing this constraints and checking the value of each term. Since this is an persistent issue across the components, we can focus on just one of the component to identify the issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_balances} : Material balances\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : (fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) - (fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_out[0.0].flow_vol) + fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solute_set, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.15 : None : True : True : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : True : True : NonNegativeReals\n", + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solute_set, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.0 : None : False : False : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : False : False : NonNegativeReals\n", + "{Member of mass_transfer_term} : Component material transfer into unit\n", + " Size=4, Index=fs._time*fs.aq_properties._phase_component_set, Units=g/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " (0.0, 'Aq', 'NaCl') : None : -31.700284300098897 : None : False : False : Reals\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems there is a discrepancy between the mass transfer term and the amount of input of NaCl. This can be inferred from the values where the input equals 15g/h and the `mass_transfer_term` equals -31.706g/h.\n", + "\n", + "To further investigate this issue, it's advisable to examine the `material_aq_balance` constraint within the unit model where the `mass_transfer_term` is defined. By printing out this constraint and analyzing its components, you can gain a better understanding of the discrepancy and take appropriate corrective actions." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_aq_balance} : Unit level material balances for Aq\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] + fs.org_properties.diffusion_factor[NaCl]*(fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here the problem can be tracked down easily as there being a typing error while recording the distribution factor. The distribution factor here was wrongly written ignoring its magnitude which should have been 1e-2, but that was missed, thus adjusting the distribution factor parameter we should have this issue resolved. " + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + ")\n", + "\n", + "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", + "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the corrective actions, we should check if this has made any structural issues, for this we would call `report_structural_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "1 Cautions\n", + "\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now since there are no warnings we can go ahead and solve the model and see if the results are optimal. " + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "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...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\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 5.85e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 3.55e-15 8.41e+00 -1.0 5.85e+01 - 1.05e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 1\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 3.5527136788005009e-15 3.5527136788005009e-15\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 3.5527136788005009e-15 3.5527136788005009e-15\n", + "\n", + "\n", + "Number of objective function evaluations = 2\n", + "Number of objective gradient evaluations = 2\n", + "Number of equality constraint evaluations = 2\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 2\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 1\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.0668952465057373}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a good sign that the model solved optimally and a solution was found. \n", + "\n", + "**NOTE:** It is a good practice to run the model through DiagnosticsToolbox regardless of the solver termination status. \n", + "\n", + "The next section we shall focus on testing the unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5. Testing\n", + "\n", + "Testing is a crucial part of model development to ensure that the model works as expected, and remains reliable. Here's an overview of why we conduct testing:\n", + "\n", + "1. `Verify Correctness`: Testing ensures that the model works as expected and meets the specified requirements. \n", + "2. `Detect Bugs and Issues`: Testing helps in identifying bugs, errors, or unexpected behaviors in the code or model, allowing for timely fixes.\n", + "3. `Ensure Reliability`: Testing improves the reliability and robustness of the software, reducing the risk of failures when the user uses it.\n", + "4. `Support Changes`: Tests provide confidence when making changes or adding new features, ensuring that existing functionalities are not affected and work as they should.\n", + "\n", + "There are typically 3 types of tests:\n", + "\n", + "1. `Unit tests`: Test runs quickly (under 2 seconds) and has no network/system dependencies. Uses only libraries installed by default with the software\n", + "2. `Component test`: Test may run more slowly (under 10 seconds, or so), e.g. it may run a solver or create a bunch of files. Like unit tests, it still shouldn't depend on special libraries or dependencies.\n", + "3. `Integration test`: Test may take a long time to run, and may have complex dependencies.\n", + "\n", + "The expectation is that unit tests should be run by developers rather frequently, component tests should be run by the continuous integration system before running code, and integration tests are run across the codebase regularly, but infrequently (e.g. daily).\n", + "\n", + "\n", + "As a developer, testing is a crucial aspect of ensuring the reliability and correctness of the unit model. The testing process involves both Unit tests and Component tests, and pytest is used as the testing framework. A typical test is marked with @pytest.mark.level, where the level indicates the depth or specificity of the testing. This is written in a file usually named as test_*.py or *_test.py. The test files have functions written in them with the appropriate level of test being conducted. \n", + "\n", + "For more detailed information on testing methodologies and procedures, developers are encouraged to refer to [this resource](https://idaes-pse.readthedocs.io/en/stable/reference_guides/developer/testing.html). The resource provides comprehensive guidance on the testing process and ensures that the unit model meets the required standards and functionality.\n", + "\n", + "## 5.1 Property package\n", + "### Unit Tests\n", + "\n", + "When writing tests for the Aqueous property phase package, it's essential to focus on key aspects to ensure the correctness and robustness of the implementation. Here are the areas to cover in the unit tests:\n", + "\n", + "1. Number of Config Dictionaries: Verify that the property phase package has the expected number of configuration dictionaries.\n", + "\n", + "2. State Block Class Name: Confirm that the correct state block class is associated with the Aqueous property phase package.\n", + "\n", + "3. Number of Phases: Check that the Aqueous property phase package defines the expected number of phases.\n", + "\n", + "4. Components in the Phase and Physical Parameter Values: Test that the components present in the Aqueous phase match the anticipated list. Additionally, validate that the physical parameter values (such as density, viscosity, etc.) are correctly defined.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "import pytest\n", + "from pyomo.environ import ConcreteModel, Param, value, Var\n", + "from pyomo.util.check_units import assert_units_consistent\n", + "from idaes.core import MaterialBalanceType, EnergyBalanceType\n", + "\n", + "from liquid_extraction.organic_property import OrgPhase\n", + "from liquid_extraction.aqueous_property import AqPhase\n", + "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", + "from idaes.core.solvers import get_solver\n", + "\n", + "solver = get_solver()\n", + "\n", + "\n", + "class TestParamBlock(object):\n", + " @pytest.fixture(scope=\"class\")\n", + " def model(self):\n", + " model = ConcreteModel()\n", + " model.params = AqPhase()\n", + " return model\n", + "\n", + " @pytest.mark.unit\n", + " def test_config(self, model):\n", + " assert len(model.params.config) == 1\n", + "\n", + " @pytest.mark.unit\n", + " def test_build(self, model):\n", + " assert len(model.params.phase_list) == 1\n", + " for i in model.params.phase_list:\n", + " assert i == \"Aq\"\n", + "\n", + " assert len(model.params.component_list) == 4\n", + " for i in model.params.component_list:\n", + " assert i in [\"H2O\", \"NaCl\", \"KNO3\", \"CaSO4\"]\n", + "\n", + " assert isinstance(model.params.cp_mass, Param)\n", + " assert value(model.params.cp_mass) == 4182\n", + "\n", + " assert isinstance(model.params.dens_mass, Param)\n", + " assert value(model.params.dens_mass) == 997\n", + "\n", + " assert isinstance(model.params.temperature_ref, Param)\n", + " assert value(model.params.temperature_ref) == 298.15" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next set of unit tests focuses on testing the build function in the state block. Here are the key aspects to cover in these tests:\n", + "\n", + "1. Existence and Initialized Values of State Variables: Verify that the state variables are correctly defined and initialized within the state block. This ensures that the state block is properly constructed and ready for initialization.\n", + "\n", + "2. Initialization Function Test: Check that state variables are not fixed before initialization and are released after initialization. This test ensures that the initialization process occurs as expected and that the state variables are appropriately managed throughout.\n", + "\n", + "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the aqueous property phase package. Similar tests can be written for the organic property package to ensure consistency and reliability across both packages." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "class TestStateBlock(object):\n", + " @pytest.fixture(scope=\"class\")\n", + " def model(self):\n", + " model = ConcreteModel()\n", + " model.params = AqPhase()\n", + "\n", + " model.props = model.params.build_state_block([1])\n", + "\n", + " return model\n", + "\n", + " @pytest.mark.unit\n", + " def test_build(self, model):\n", + " assert isinstance(model.props[1].flow_vol, Var)\n", + " assert value(model.props[1].flow_vol) == 1\n", + "\n", + " assert isinstance(model.props[1].temperature, Var)\n", + " assert value(model.props[1].temperature) == 300\n", + "\n", + " assert isinstance(model.props[1].conc_mass_comp, Var)\n", + " assert len(model.props[1].conc_mass_comp) == 3\n", + "\n", + " @pytest.mark.unit\n", + " def test_initialize(self, model):\n", + " assert not model.props[1].flow_vol.fixed\n", + " assert not model.props[1].temperature.fixed\n", + " assert not model.props[1].pressure.fixed\n", + " for i in model.props[1].conc_mass_comp:\n", + " assert not model.props[1].conc_mass_comp[i].fixed\n", + "\n", + " model.props.initialize(hold_state=False, outlvl=1)\n", + "\n", + " assert not model.props[1].flow_vol.fixed\n", + " assert not model.props[1].temperature.fixed\n", + " assert not model.props[1].pressure.fixed\n", + " for i in model.props[1].conc_mass_comp:\n", + " assert not model.props[1].conc_mass_comp[i].fixed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Component Tests\n", + "In the component test, we aim to ensure unit consistency across the entire property package. Unlike unit tests that focus on individual functions, component tests assess the coherence and consistency of the entire package. Here's what the component test will entail:\n", + "\n", + "Unit Consistency Check: Verify that all units used within the property package are consistent throughout. This involves checking that all parameters, variables, and equations within the package adhere to the same unit system, ensuring compatibility.\n", + "\n", + "By conducting a comprehensive component test, we can ensure that the property package functions as a cohesive unit, maintaining consistency and reliability across its entirety. This concludes our tests on the property package. Next we shall test the unit model. " + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [], + "source": [ + "@pytest.mark.component\n", + "def check_units(model):\n", + " model = ConcreteModel()\n", + " model.params = AqPhase()\n", + " assert_units_consistent(model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5.2 Unit Model\n", + "### Unit tests\n", + "Unit tests for the unit model encompass verifying the configuration arguments and the build function, similar to the approach taken for the property package. When testing the config arguments, we ensure that the correct number of arguments is provided and then match each argument with the expected one. This ensures that the unit model is properly configured and ready to operate as intended." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "import pytest\n", + "\n", + "from idaes.core import FlowsheetBlock\n", + "import idaes.models.unit_models\n", + "from idaes.core.solvers import get_solver\n", + "import idaes.logger as idaeslog\n", + "\n", + "\n", + "from pyomo.environ import value, check_optimal_termination, units\n", + "from pyomo.util.check_units import assert_units_consistent\n", + "from idaes.core.util.model_statistics import (\n", + " number_variables,\n", + " number_total_constraints,\n", + ")\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.initialization import (\n", + " SingleControlVolumeUnitInitializer,\n", + ")\n", + "\n", + "solver = get_solver()\n", + "\n", + "\n", + "@pytest.mark.unit\n", + "def test_config():\n", + " m = ConcreteModel()\n", + " m.fs = FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.unit = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + "\n", + " # Check unit config arguments\n", + " assert len(m.fs.unit.config) == 9\n", + "\n", + " # Check for config arguments\n", + " assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault\n", + " assert not m.fs.unit.config.has_pressure_change\n", + " assert not m.fs.unit.config.has_phase_equilibrium\n", + " assert m.fs.unit.config.organic_property_package is m.fs.org_properties\n", + " assert m.fs.unit.config.aqueous_property_package is m.fs.aq_properties\n", + "\n", + " # Check for unit initializer\n", + " assert m.fs.unit.default_initializer is SingleControlVolumeUnitInitializer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In testing the build function, we verify whether the number of variables aligns with the intended values and also check for the existence of desired constraints within the unit model. This ensures that the unit model is constructed accurately and includes all the necessary variables and constraints required for its proper functioning." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [], + "source": [ + "class TestBuild(object):\n", + " @pytest.fixture(scope=\"class\")\n", + " def model(self):\n", + " m = ConcreteModel()\n", + " m.fs = FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.unit = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + "\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.l / units.h)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.l)\n", + "\n", + " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.l / units.h)\n", + " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.l)\n", + "\n", + " return m\n", + "\n", + " @pytest.mark.build\n", + " @pytest.mark.unit\n", + " def test_build(self, model):\n", + "\n", + " assert hasattr(model.fs.unit, \"aqueous_inlet\")\n", + " assert len(model.fs.unit.aqueous_inlet.vars) == 4\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"organic_inlet\")\n", + " assert len(model.fs.unit.organic_inlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_inlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"aqueous_outlet\")\n", + " assert len(model.fs.unit.aqueous_outlet.vars) == 4\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"organic_outlet\")\n", + " assert len(model.fs.unit.organic_outlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_outlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"material_aq_balance\")\n", + " assert hasattr(model.fs.unit, \"material_org_balance\")\n", + "\n", + " assert number_variables(model) == 34\n", + " assert number_total_constraints(model) == 16" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Component tests\n", + "\n", + "During the component tests, we evaluate the performance of the unit model when integrated with the property package. This evaluation process typically involves several steps:\n", + "\n", + "1. Unit Consistency Check: Verify that the unit model maintains consistency in its units throughout the model. This ensures that all variables and constraints within the model adhere to the same unit system, guaranteeing compatibility.\n", + "\n", + "2. Termination Condition Verification: This involves checking whether the model terminates optimally with the given inlet conditions.\n", + "\n", + "3. Variable Value Assessment: Check the values of outlet variables against the expected values. To account for the numerical tolerance of the solvers, the values are compared using the approx function with a relative tolerance.\n", + "\n", + "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered.\n", + "\n", + "5. Structural Issues: Verify that there are no structural issues with the model. \n", + "\n", + "By performing these checks, we conclude the testing for the unit model. " + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "class TestFlowsheet:\n", + " @pytest.fixture\n", + " def model(self):\n", + " m = ConcreteModel()\n", + " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.unit = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + " )\n", + "\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", + "\n", + " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", + " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.kg)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.kg)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.kg)\n", + "\n", + " return m\n", + "\n", + " @pytest.mark.component\n", + " def test_unit_model(self, model):\n", + " assert_units_consistent(model)\n", + " solver = get_solver()\n", + " results = solver.solve(model, tee=False)\n", + "\n", + " # Check for optimal termination\n", + " assert check_optimal_termination(results)\n", + "\n", + " # Checking for outlet flows\n", + " assert value(model.fs.unit.organic_outlet.flow_vol[0]) == pytest.approx(\n", + " 80.0, rel=1e-5\n", + " )\n", + " assert value(model.fs.unit.aqueous_outlet.flow_vol[0]) == pytest.approx(\n", + " 10.0, rel=1e-5\n", + " )\n", + "\n", + " # Checking for outlet mass_comp\n", + " assert value(\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"CaSO4\"]\n", + " ) == pytest.approx(0.000187499, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"KNO3\"]\n", + " ) == pytest.approx(0.000749999, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"NaCl\"]\n", + " ) == pytest.approx(0.000403124, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"CaSO4\"]\n", + " ) == pytest.approx(0.0985, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"KNO3\"]\n", + " ) == pytest.approx(0.194, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"NaCl\"]\n", + " ) == pytest.approx(0.146775, rel=1e-5)\n", + "\n", + " # Checking for outlet temperature\n", + " assert value(model.fs.unit.organic_outlet.temperature[0]) == pytest.approx(\n", + " 300, rel=1e-5\n", + " )\n", + " assert value(model.fs.unit.aqueous_outlet.temperature[0]) == pytest.approx(\n", + " 300, rel=1e-5\n", + " )\n", + "\n", + " # Checking for outlet pressure\n", + " assert value(model.fs.unit.organic_outlet.pressure[0]) == pytest.approx(\n", + " 1, rel=1e-5\n", + " )\n", + " assert value(model.fs.unit.aqueous_outlet.pressure[0]) == pytest.approx(\n", + " 1, rel=1e-5\n", + " )\n", + "\n", + " # Fixed state variables\n", + " assert model.fs.unit.organic_inlet.flow_vol[0].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", + " assert model.fs.unit.organic_inlet.temperature[0].fixed\n", + " assert model.fs.unit.organic_inlet.pressure[0].fixed\n", + "\n", + " assert model.fs.unit.aqueous_inlet.flow_vol[0].fixed\n", + " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", + " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", + " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", + " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", + " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", + "\n", + " @pytest.mark.component\n", + " def test_structural_issues(self, model):\n", + " dt = DiagnosticsToolbox(model)\n", + " dt.assert_no_structural_warnings()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this tutorial, we have covered the comprehensive process of creating a custom unit model from scratch. Let's recap the key steps we have undertaken:\n", + "\n", + "- Developing property package\n", + "- Constructing the unit model \n", + "- Creating a Flowsheet\n", + "- Debugging the model using DiagnosticsToolbox\n", + "- Writing tests for the unit model\n", + "\n", + "By following the aforementioned procedure, one can create their own custom unit model. This concludes the tutorial on creating a custom unit model. " ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" } - ], - "source": [ - "solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is a good sign that the model solved optimally and a solution was found. \n", - "\n", - "**NOTE:** It is a good practice to run the model through DiagnosticsToolbox regardless of the solver termination status. \n", - "\n", - "The next section we shall focus on testing the unit model. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 5. Testing\n", - "\n", - "Testing is a crucial part of model development to ensure that the model works as expected, and remains reliable. Here's an overview of why we conduct testing:\n", - "\n", - "1. `Verify Correctness`: Testing ensure that the model works as expected and meets the specified requirements. \n", - "2. `Detect Bugs and Issues`: Testing helps in identifying bugs, errors, or unexpected behaviors in the code or model, allowing for timely fixes.\n", - "3. `Ensure Reliability`: Testing improves the reliability and robustness of the software, reducing the risk of failures when the user uses it.\n", - "4. `Support Changes`: Tests provide confidence when making changes or adding new features, ensuring that existing functionalities are not affected and work as they should.\n", - "\n", - "There are typically 3 types of tests:\n", - "\n", - "1. `Unit tests`: Test runs quickly (under 2 seconds) and has no network/system dependencies. Uses only libraries installed by default with the software\n", - "2. `Component test`: Test may run more slowly (under 10 seconds, or so), e.g. it may run a solver or create a bunch of files. Like unit tests, it still shouldn't depend on special libraries or dependencies.\n", - "3. `Integration test`: Test may take a long time to run, and may have complex dependencies.\n", - "\n", - "The expectation is that unit tests should be run by developers rather frequently, component tests should be run by the continuous integration system before running code, and integration tests are run across the codebase regularly, but infrequently (e.g. daily).\n", - "\n", - "\n", - "As a developer, testing is a crucial aspect of ensuring the reliability and correctness of the unit model. The testing process involves both Unit tests and Component tests, and pytest is used as the testing framework. A typical test is marked with @pytest.mark.level, where the level indicates the depth or specificity of the testing. This is written in a file usually named as test_*.py or *_test.py. The test files have functions written in them with the appropriate level of test being conducted. \n", - "\n", - "For more detailed information on testing methodologies and procedures, developers are encouraged to refer to [this resource](https://idaes-pse.readthedocs.io/en/stable/reference_guides/developer/testing.html). The resource provides comprehensive guidance on the testing process and ensures that the unit model meets the required standards and functionality.\n", - "\n", - "## 5.1 Property package\n", - "### Unit Tests\n", - "\n", - "When writing tests for the Aqueous property phase package, it's essential to focus on key aspects to ensure the correctness and robustness of the implementation. Here are the areas to cover in the unit tests:\n", - "\n", - "1. Number of Config Dictionaries: Verify that the property phase package has the expected number of configuration dictionaries.\n", - "\n", - "2. State Block Class Name: Confirm that the correct state block class is associated with the Aqueous property phase package.\n", - "\n", - "3. Number of Phases: Check that the Aqueous property phase package defines the expected number of phases.\n", - "\n", - "4. Components in the Phase and Physical Parameter Values: Test that the components present in the Aqueous phase match the anticipated list. Additionally, validate that the physical parameter values (such as density, viscosity, etc.) are correctly defined.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "import pytest\n", - "from pyomo.environ import ConcreteModel, Param, value, Var\n", - "from pyomo.util.check_units import assert_units_consistent\n", - "from idaes.core import MaterialBalanceType, EnergyBalanceType\n", - "\n", - "from liquid_extraction.organic_property import OrgPhase\n", - "from liquid_extraction.aqueous_property import AqPhase\n", - "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", - "from idaes.core.solvers import get_solver\n", - "\n", - "solver = get_solver()\n", - "\n", - "\n", - "class TestParamBlock(object):\n", - " @pytest.fixture(scope=\"class\")\n", - " def model(self):\n", - " model = ConcreteModel()\n", - " model.params = AqPhase()\n", - " return model\n", - "\n", - " @pytest.mark.unit\n", - " def test_config(self, model):\n", - " assert len(model.params.config) == 1\n", - "\n", - " @pytest.mark.unit\n", - " def test_build(self, model):\n", - " assert len(model.params.phase_list) == 1\n", - " for i in model.params.phase_list:\n", - " assert i == \"Aq\"\n", - "\n", - " assert len(model.params.component_list) == 4\n", - " for i in model.params.component_list:\n", - " assert i in [\"H2O\", \"NaCl\", \"KNO3\", \"CaSO4\"]\n", - "\n", - " assert isinstance(model.params.cp_mass, Param)\n", - " assert value(model.params.cp_mass) == 4182\n", - "\n", - " assert isinstance(model.params.dens_mass, Param)\n", - " assert value(model.params.dens_mass) == 997\n", - "\n", - " assert isinstance(model.params.temperature_ref, Param)\n", - " assert value(model.params.temperature_ref) == 298.15" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next set of unit tests focuses on testing the build function in the state block. Here are the key aspects to cover in these tests:\n", - "\n", - "1. Existence and Initialized Values of State Variables: Verify that the state variables are correctly defined and initialized within the state block. This ensures that the state block is properly constructed and ready for initialization.\n", - "\n", - "2. Initialization Function Test: Check that state variables are not fixed before initialization and are released after initialization. This test ensures that the initialization process occurs as expected and that the state variables are appropriately managed throughout.\n", - "\n", - "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the Aqueous property phase package. Similar tests can be written for the organic property package to ensure consistency and reliability across both packages." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "class TestStateBlock(object):\n", - " @pytest.fixture(scope=\"class\")\n", - " def model(self):\n", - " model = ConcreteModel()\n", - " model.params = AqPhase()\n", - "\n", - " model.props = model.params.build_state_block([1])\n", - "\n", - " return model\n", - "\n", - " @pytest.mark.unit\n", - " def test_build(self, model):\n", - " assert isinstance(model.props[1].flow_vol, Var)\n", - " assert value(model.props[1].flow_vol) == 1\n", - "\n", - " assert isinstance(model.props[1].temperature, Var)\n", - " assert value(model.props[1].temperature) == 300\n", - "\n", - " assert isinstance(model.props[1].conc_mass_comp, Var)\n", - " assert len(model.props[1].conc_mass_comp) == 3\n", - "\n", - " @pytest.mark.unit\n", - " def test_initialize(self, model):\n", - " assert not model.props[1].flow_vol.fixed\n", - " assert not model.props[1].temperature.fixed\n", - " assert not model.props[1].pressure.fixed\n", - " for i in model.props[1].conc_mass_comp:\n", - " assert not model.props[1].conc_mass_comp[i].fixed\n", - "\n", - " model.props.initialize(hold_state=False, outlvl=1)\n", - "\n", - " assert not model.props[1].flow_vol.fixed\n", - " assert not model.props[1].temperature.fixed\n", - " assert not model.props[1].pressure.fixed\n", - " for i in model.props[1].conc_mass_comp:\n", - " assert not model.props[1].conc_mass_comp[i].fixed" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Component Tests\n", - "In the component test, we aim to ensure unit consistency across the entire property package. Unlike unit tests that focus on individual functions, component tests assess the coherence and consistency of the entire package. Here's what the component test will entail:\n", - "\n", - "Unit Consistency Check: Verify that all units used within the property package are consistent throughout. This involves checking that all parameters, variables, and equations within the package adhere to the same unit system, ensuring compatibility.\n", - "\n", - "By conducting a comprehensive component test, we can ensure that the property package functions as a cohesive unit, maintaining consistency and reliability across its entirety. This concludes our tests on the property package. Next we shall test the unit model. " - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "@pytest.mark.component\n", - "def check_units(model):\n", - " model = ConcreteModel()\n", - " model.params = AqPhase()\n", - " assert_units_consistent(model)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 5.2 Unit Model\n", - "### Unit tests\n", - "Unit tests for the unit model encompass verifying the configuration arguments and the build function, similar to the approach taken for the property package. When testing the config arguments, we ensure that the correct number of arguments is provided and then match each argument with the expected one. This ensures that the unit model is properly configured and ready to operate as intended." - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [], - "source": [ - "import pytest\n", - "\n", - "import idaes.core\n", - "import idaes.models.unit_models\n", - "from idaes.core.solvers import get_solver\n", - "import idaes.logger as idaeslog\n", - "\n", - "\n", - "from pyomo.environ import value, check_optimal_termination, units\n", - "from pyomo.util.check_units import assert_units_consistent\n", - "from idaes.core.util.model_statistics import (\n", - " number_variables,\n", - " number_total_constraints,\n", - ")\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.initialization import (\n", - " SingleControlVolumeUnitInitializer,\n", - ")\n", - "\n", - "solver = get_solver()\n", - "\n", - "\n", - "@pytest.mark.unit\n", - "def test_config():\n", - " m = ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.unit = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - "\n", - " # Check unit config arguments\n", - " assert len(m.fs.unit.config) == 9\n", - "\n", - " # Check for config arguments\n", - " assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault\n", - " assert not m.fs.unit.config.has_pressure_change\n", - " assert not m.fs.unit.config.has_phase_equilibrium\n", - " assert m.fs.unit.config.organic_property_package is m.fs.org_properties\n", - " assert m.fs.unit.config.aqueous_property_package is m.fs.aq_properties\n", - "\n", - " # Check for unit initializer\n", - " assert m.fs.unit.default_initializer is SingleControlVolumeUnitInitializer" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In testing the build function, we verify whether the number of variables aligns with the intended values and also check for the existence of desired constraints within the unit model. This ensures that the unit model is constructed accurately and includes all the necessary variables and constraints required for its proper functioning." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "class TestBuild(object):\n", - " @pytest.fixture(scope=\"class\")\n", - " def model(self):\n", - " m = ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.unit = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - "\n", - " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.l / units.h)\n", - " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.l)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.l)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.l)\n", - "\n", - " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.l / units.h)\n", - " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.l)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.l)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.l)\n", - "\n", - " return m\n", - "\n", - " @pytest.mark.build\n", - " @pytest.mark.unit\n", - " def test_build(self, model):\n", - "\n", - " assert hasattr(model.fs.unit, \"aqueous_inlet\")\n", - " assert len(model.fs.unit.aqueous_inlet.vars) == 4\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"organic_inlet\")\n", - " assert len(model.fs.unit.organic_inlet.vars) == 4\n", - " assert hasattr(model.fs.unit.organic_inlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.organic_inlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.organic_inlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.organic_inlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"aqueous_outlet\")\n", - " assert len(model.fs.unit.aqueous_outlet.vars) == 4\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"organic_outlet\")\n", - " assert len(model.fs.unit.organic_outlet.vars) == 4\n", - " assert hasattr(model.fs.unit.organic_outlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.organic_outlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.organic_outlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.organic_outlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"material_aq_balance\")\n", - " assert hasattr(model.fs.unit, \"material_org_balance\")\n", - "\n", - " assert number_variables(model) == 34\n", - " assert number_total_constraints(model) == 16" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Component tests\n", - "\n", - "During the component tests, we evaluate the performance of the unit model when integrated with the property package. This evaluation process typically involves several steps:\n", - "\n", - "1. Unit Consistency Check: Verify that the unit model maintains consistency in its units throughout the model. This ensures that all variables and constraints within the model adhere to the same unit system, guaranteeing compatibility.\n", - "\n", - "2. Termination Condition Verification: This involves checking whether the model terminates optimally with the given inlet conditions.\n", - "\n", - "3. Variable Value Assessment: Check the values of outlet variables against the expected values. To account for the numerical tolerance of the solvers, the values are compared using the approx function with a relative tolerance.\n", - "\n", - "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered.\n", - "\n", - "5. Structural Issues: Verify that there are no structural issues with the model. \n", - "\n", - "By performing these checks, we conclude the testing for the unit model. " - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [], - "source": [ - "class TestFlowsheet:\n", - " @pytest.fixture\n", - " def model(self):\n", - " m = ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.unit = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", - " )\n", - " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", - " )\n", - " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", - " )\n", - "\n", - " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", - " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", - "\n", - " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", - " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.kg)\n", - "\n", - " return m\n", - "\n", - " @pytest.mark.component\n", - " def test_unit_model(self, model):\n", - " assert_units_consistent(model)\n", - " solver = get_solver()\n", - " results = solver.solve(model, tee=False)\n", - "\n", - " # Check for optimal termination\n", - " assert check_optimal_termination(results)\n", - "\n", - " # Checking for outlet flows\n", - " assert value(model.fs.unit.organic_outlet.flow_vol[0]) == pytest.approx(\n", - " 80.0, rel=1e-5\n", - " )\n", - " assert value(model.fs.unit.aqueous_outlet.flow_vol[0]) == pytest.approx(\n", - " 10.0, rel=1e-5\n", - " )\n", - "\n", - " # Checking for outlet mass_comp\n", - " assert value(\n", - " model.fs.unit.organic_outlet.conc_mass_comp[0, \"CaSO4\"]\n", - " ) == pytest.approx(0.000187499, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.organic_outlet.conc_mass_comp[0, \"KNO3\"]\n", - " ) == pytest.approx(0.000749999, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.organic_outlet.conc_mass_comp[0, \"NaCl\"]\n", - " ) == pytest.approx(0.000403124, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"CaSO4\"]\n", - " ) == pytest.approx(0.0985, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"KNO3\"]\n", - " ) == pytest.approx(0.194, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"NaCl\"]\n", - " ) == pytest.approx(0.146775, rel=1e-5)\n", - "\n", - " # Checking for outlet temperature\n", - " assert value(model.fs.unit.organic_outlet.temperature[0]) == pytest.approx(\n", - " 300, rel=1e-5\n", - " )\n", - " assert value(model.fs.unit.aqueous_outlet.temperature[0]) == pytest.approx(\n", - " 300, rel=1e-5\n", - " )\n", - "\n", - " # Checking for outlet pressure\n", - " assert value(model.fs.unit.organic_outlet.pressure[0]) == pytest.approx(\n", - " 1, rel=1e-5\n", - " )\n", - " assert value(model.fs.unit.aqueous_outlet.pressure[0]) == pytest.approx(\n", - " 1, rel=1e-5\n", - " )\n", - "\n", - " # Fixed state variables\n", - " assert model.fs.unit.organic_inlet.flow_vol[0].fixed\n", - " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", - " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", - " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", - " assert model.fs.unit.organic_inlet.temperature[0].fixed\n", - " assert model.fs.unit.organic_inlet.pressure[0].fixed\n", - "\n", - " assert model.fs.unit.aqueous_inlet.flow_vol[0].fixed\n", - " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", - " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", - " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", - " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", - " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", - "\n", - " @pytest.mark.component\n", - " def test_structural_issues(self, model):\n", - " dt = DiagnosticsToolbox(model)\n", - " dt.assert_no_structural_warnings()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this tutorial, we have covered the comprehensive process of creating a custom unit model from scratch. Let's recap the key steps we have undertaken:\n", - "\n", - "- Developing property package\n", - "- Constructing the unit model \n", - "- Creating a Flowsheet\n", - "- Debugging the model using DiagnosticsToolbox\n", - "- Writing tests for the unit model\n", - "\n", - "By following the aforementioned procedure, one can create their own custom unit model. This would conclude the tutorial on creating custom unit model. " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "idaes-pse", - "language": "python", - "name": "python3" + ], + "metadata": { + "kernelspec": { + "display_name": "examples-310-new", + "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.10.19" + } }, - "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.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 3 -} + "nbformat": 4, + "nbformat_minor": 3 +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_test.ipynb b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_test.ipynb index f11bab48..21aca1ae 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_test.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_test.ipynb @@ -1,1964 +1,2408 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "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": [ - "# Creating Custom Unit Model\n", - "Author: Javal Vyas \n", - "Maintainer: Javal Vyas \n", - "Updated: 2023-02-20\n", - "\n", - "This tutorial is a comprehensive step-wise procedure to build a custom unit model from scratch. This tutorial will include creating a property package, a custom unit model and testing them. For this tutorial we shall create a custom unit model for Liquid - Liquid Extraction. \n", - "\n", - "The Liquid - Liquid Extractor model contains two immiscible fluids forming the two phases. One of the phases, say phase_1 has a high concentration of solutes which is to be separated. A mass transfer happens between the two phases and the solute is transferred from phase_1 to phase_2. This mass transfer is governed by a parameter called the distribution coefficient.\n", - "\n", - "After reviewing the working principles of the Liquid - Liquid Extractor, we shall proceed to create a custom unit model. We will require a property package for each phase, a custom unit model class and tests for the model and property packages.\n", - "\n", - "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", - "- Steady-state only\n", - "- Organic phase property package has a single phase named Org\n", - "- Aqueous phase property package has a single phase named Aq\n", - "- Organic and Aqueous phase properties need not have the same component list. \n", - "\n", - "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1. Creating Organic Property Package\n", - "\n", - "Creating a property package is a 4 step process\n", - "- Import necessary libraries \n", - "- Creating Physical Parameter Data Block\n", - "- Define State Block\n", - "- Define State Block Data\n", - "\n", - "# 1.1 Importing necessary packages \n", - "Let us begin with the importing the necessary libraries where we will be using functionalities from IDAES and Pyomo. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Import Python libraries\n", - "import logging\n", - "\n", - "import idaes.logger as idaeslog\n", - "from idaes.core.util.initialization import fix_state_vars\n", - "\n", - "# Import Pyomo libraries\n", - "from pyomo.environ import (\n", - " Param,\n", - " Set,\n", - " Var,\n", - " NonNegativeReals,\n", - " units,\n", - " Expression,\n", - " PositiveReals,\n", - ")\n", - "\n", - "# Import IDAES cores\n", - "from idaes.core import (\n", - " declare_process_block_class,\n", - " MaterialFlowBasis,\n", - " PhysicalParameterBlock,\n", - " StateBlockData,\n", - " StateBlock,\n", - " MaterialBalanceType,\n", - " EnergyBalanceType,\n", - " Solute,\n", - " Solvent,\n", - " LiquidPhase,\n", - ")\n", - "from idaes.core.util.model_statistics import degrees_of_freedom" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1.2 Physical Parameter Data Block\n", - "\n", - "A `PhysicalParameterBlock` serves as the central point of reference for all aspects of the property package and needs to define several things about the package. These are summarized below:\n", - "\n", - "- Units of measurement\n", - "- What properties are supported and how they are implemented\n", - "- What components and phases are included in the packages\n", - "- All the global parameters necessary for calculating properties\n", - "- A reference to the associated State Block class, so that construction of the State Block components can be automated from the Physical Parameter Block\n", - "\n", - "To construct this block, we begin by declaring a process block class using a Python decorator. One can learn more about `declare_process_block_class` [here](https://github.com/IDAES/idaes-pse/blob/eea1209077b75f7d940d8958362e69d4650c079d/idaes/core/base/process_block.py#L173). After constructing the process block, we define a build function which contains all the components that the property package would have. `super` function here is used to give access to methods and properties of a parent or sibling class and since this is used on the class `PhysicalParameterData` class, build has access to all the parent and sibling class methods. \n", - "\n", - "The `PhysicalParameterBlock` then refers to the `state block`, in this case `OrgPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we move on to list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. Like here since the solvent is in the Organic phase, we will assign the Phase as OrganicPhase and the variable will be named Org as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", - " \n", - "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the Organic phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. Additionally, a parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", - "\n", - "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). In this method, we will call the predefined method `add_default_units()`.\n", - "\n", - "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@declare_process_block_class(\"OrgPhase\")\n", - "class PhysicalParameterData(PhysicalParameterBlock):\n", - " \"\"\"\n", - " Property Parameter Block Class\n", - "\n", - " Contains parameters and indexing sets associated with properties for\n", - " organic Phase\n", - "\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction.\n", - " \"\"\"\n", - " super().build()\n", - "\n", - " self._state_block_class = OrgPhaseStateBlock\n", - "\n", - " # List of valid phases in property package\n", - " self.Org = LiquidPhase()\n", - "\n", - " # Component list - a list of component identifiers\n", - " self.NaCl = Solute()\n", - " self.KNO3 = Solute()\n", - " self.CaSO4 = Solute()\n", - " self.solvent = (\n", - " Solvent()\n", - " ) # Solvent used here is ethylene dibromide (Organic Polar)\n", - "\n", - " # Heat capacity of solvent\n", - " self.cp_mass = Param(\n", - " mutable=True,\n", - " initialize=717.01,\n", - " doc=\"Specific heat capacity of solvent\",\n", - " units=units.J / units.kg / units.K,\n", - " )\n", - "\n", - " self.dens_mass = Param(\n", - " mutable=True,\n", - " initialize=2170,\n", - " doc=\"Density of ethylene dibromide\",\n", - " units=units.kg / units.m**3,\n", - " )\n", - " self.temperature_ref = Param(\n", - " within=PositiveReals,\n", - " mutable=True,\n", - " default=298.15,\n", - " doc=\"Reference temperature\",\n", - " units=units.K,\n", - " )\n", - " self.diffusion_factor = Param(\n", - " self.solute_set,\n", - " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", - " within=PositiveReals,\n", - " mutable=True,\n", - " )\n", - "\n", - " @classmethod\n", - " def define_metadata(cls, obj):\n", - " obj.add_default_units(\n", - " {\n", - " \"time\": units.hour,\n", - " \"length\": units.m,\n", - " \"mass\": units.g,\n", - " \"amount\": units.mol,\n", - " \"temperature\": units.K,\n", - " }\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1.3 State Block\n", - "\n", - "After the `PhysicalParameterBlock` class has been created, the next step is to write the code necessary to create the State Blocks that will be used throughout the flowsheet. `StateBlock` contains all the information necessary to define the state of the system. This includes the state variables and constraints on those variables which are used to describe a state property like the enthalpy, material balance, etc.\n", - "\n", - "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed StateBlock all at once (rather than element by element).\n", - "\n", - "The class `_OrganicStateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the `fix_initialization_states` function. `fix_initialization_states` function is to used to fix the state variable within the state block with the provided initial values (usually inlet conditions). It takes a `block` as the argument in which the state variables are to be fixed. It also takes `state_args` as an optional argument. `state_args` is a dictionary with the value for the state variables to be fixed. This function returns a dictionary indexed by the block, state variables and variable index indicating the fixed status of each variable before applying the function. \n", - "\n", - "The above function comprise of the _OrganicStateBlock, next we shall see the construction of the OrgPhaseStateBlockData class." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class _OrganicStateBlock(StateBlock):\n", - " \"\"\"\n", - " This Class contains methods which should be applied to Property Blocks as a\n", - " whole, rather than individual elements of indexed Property Blocks.\n", - " \"\"\"\n", - "\n", - " def fix_initialization_states(self):\n", - " fix_state_vars(self)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The class `OrgPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `OrgPhaseStateBlock`, and inherits the block class from `_OrganicStateBlock`. This inheritance allows `OrgPhaseStateBlockData` to leverage functions from `_OrganicStateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", - "\n", - "The subsequent objective is to delineate the state variables, accomplished through the `_make_state_vars` method. This method encompasses all the essential state variables and associated data. For this particular property package, the required state variables are:\n", - "\n", - "- `flow_vol` - volumetric flow rate\n", - "- `conc_mass_comp` - mass fractions\n", - "- `pressure` - state pressure\n", - "- `temperature` - state temperature\n", - "\n", - "After establishing the state variables, the subsequent step involves setting up state properties as constraints. This includes specifying the relationships and limitations that dictate the system's behavior. The following properties need to be articulated:\n", - "\n", - "-`get_material_flow_terms`: quantifies the amount of material flow.\n", - "- `get_enthalpy_flow_terms`: quantifies the amount of enthalpy flow.\n", - "- `get_flow_rate`: details volumetric flow rates.\n", - "- `default_material_balance_type`: defines the kind of material balance to be used.\n", - "- `default_energy_balance_type`: defines the kind of energy balance to be used.\n", - "- `define_state_vars`: involves defining state variables with units, akin to the define_metadata function in the PhysicalParameterData block.\n", - "- `get_material_flow_basis`: establishes the basis on which state variables are measured, whether in mass or molar terms.\n", - "\n", - "These definitions mark the conclusion of the state block construction and thus the property package. For additional details on creating a property package, please refer to this [resource](../../properties/custom/custom_physical_property_packages_test.ipynb ).\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_OrganicStateBlock)\n", - "class OrgPhaseStateBlockData(StateBlockData):\n", - " \"\"\"\n", - " An example property package for Organic phzase for liquid liquid extraction\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction\n", - " \"\"\"\n", - " super().build()\n", - " self._make_state_vars()\n", - "\n", - " def _make_state_vars(self):\n", - " self.flow_vol = Var(\n", - " initialize=1,\n", - " domain=NonNegativeReals,\n", - " doc=\"Total volumetric flowrate\",\n", - " units=units.L / units.hour,\n", - " )\n", - " self.conc_mass_comp = Var(\n", - " self.params.solute_set,\n", - " domain=NonNegativeReals,\n", - " initialize=1,\n", - " doc=\"Component mass concentrations\",\n", - " units=units.g / units.L,\n", - " )\n", - " self.pressure = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=1,\n", - " bounds=(1, 5),\n", - " units=units.atm,\n", - " doc=\"State pressure [atm]\",\n", - " )\n", - "\n", - " self.temperature = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=300,\n", - " bounds=(273, 373),\n", - " units=units.K,\n", - " doc=\"State temperature [K]\",\n", - " )\n", - "\n", - " def material_flow_expression(self, j):\n", - " if j == \"solvent\":\n", - " return self.flow_vol * self.params.dens_mass\n", - " else:\n", - " return self.flow_vol * self.conc_mass_comp[j]\n", - "\n", - " self.material_flow_expression = Expression(\n", - " self.component_list,\n", - " rule=material_flow_expression,\n", - " doc=\"Material flow terms\",\n", - " )\n", - "\n", - " def enthalpy_flow_expression(self):\n", - " return (\n", - " self.flow_vol\n", - " * self.params.dens_mass\n", - " * self.params.cp_mass\n", - " * (self.temperature - self.params.temperature_ref)\n", - " )\n", - "\n", - " self.enthalpy_flow_expression = Expression(\n", - " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", - " )\n", - "\n", - " def get_flow_rate(self):\n", - " return self.flow_vol\n", - "\n", - " def get_material_flow_terms(self, p, j):\n", - " return self.material_flow_expression[j]\n", - "\n", - " def get_enthalpy_flow_terms(self, p):\n", - " return self.enthalpy_flow_expression\n", - "\n", - " def default_material_balance_type(self):\n", - " return MaterialBalanceType.componentTotal\n", - "\n", - " def default_energy_balance_type(self):\n", - " return EnergyBalanceType.enthalpyTotal\n", - "\n", - " def define_state_vars(self):\n", - " return {\n", - " \"flow_vol\": self.flow_vol,\n", - " \"conc_mass_comp\": self.conc_mass_comp,\n", - " \"temperature\": self.temperature,\n", - " \"pressure\": self.pressure,\n", - " }\n", - "\n", - " def get_material_flow_basis(self):\n", - " return MaterialFlowBasis.mass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 2. Creating Aqueous Property Package\n", - "\n", - "The structure of Aqueous Property Package mirrors that of the Organic Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Import Python libraries\n", - "import logging\n", - "\n", - "from idaes.core.util.initialization import fix_state_vars\n", - "\n", - "# Import Pyomo libraries\n", - "from pyomo.environ import (\n", - " Param,\n", - " Var,\n", - " NonNegativeReals,\n", - " units,\n", - " Expression,\n", - " PositiveReals,\n", - ")\n", - "\n", - "# Import IDAES cores\n", - "from idaes.core import (\n", - " declare_process_block_class,\n", - " MaterialFlowBasis,\n", - " PhysicalParameterBlock,\n", - " StateBlockData,\n", - " StateBlock,\n", - " MaterialBalanceType,\n", - " EnergyBalanceType,\n", - " Solute,\n", - " Solvent,\n", - " LiquidPhase,\n", - ")\n", - "\n", - "# Some more information about this module\n", - "__author__ = \"Javal Vyas\"\n", - "\n", - "\n", - "# Set up logger\n", - "_log = logging.getLogger(__name__)\n", - "\n", - "\n", - "@declare_process_block_class(\"AqPhase\")\n", - "class AqPhaseData(PhysicalParameterBlock):\n", - " \"\"\"\n", - " Property Parameter Block Class\n", - "\n", - " Contains parameters and indexing sets associated with properties for\n", - " aqueous Phase\n", - "\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction.\n", - " \"\"\"\n", - " super().build()\n", - "\n", - " self._state_block_class = AqPhaseStateBlock\n", - "\n", - " # List of valid phases in property package\n", - " self.Aq = LiquidPhase()\n", - "\n", - " # Component list - a list of component identifiers\n", - " self.NaCl = Solute()\n", - " self.KNO3 = Solute()\n", - " self.CaSO4 = Solute()\n", - " self.H2O = Solvent()\n", - "\n", - " # Heat capacity of solvent\n", - " self.cp_mass = Param(\n", - " mutable=True,\n", - " initialize=4182,\n", - " doc=\"Specific heat capacity of solvent\",\n", - " units=units.J / units.kg / units.K,\n", - " )\n", - "\n", - " self.dens_mass = Param(\n", - " mutable=True,\n", - " initialize=997,\n", - " doc=\"Density of ethylene dibromide\",\n", - " units=units.kg / units.m**3,\n", - " )\n", - " self.temperature_ref = Param(\n", - " within=PositiveReals,\n", - " mutable=True,\n", - " default=298.15,\n", - " doc=\"Reference temperature\",\n", - " units=units.K,\n", - " )\n", - "\n", - " @classmethod\n", - " def define_metadata(cls, obj):\n", - " obj.add_default_units(\n", - " {\n", - " \"time\": units.hour,\n", - " \"length\": units.m,\n", - " \"mass\": units.g,\n", - " \"amount\": units.mol,\n", - " \"temperature\": units.K,\n", - " }\n", - " )\n", - "\n", - "\n", - "class _AqueousStateBlock(StateBlock):\n", - " \"\"\"\n", - " This Class contains methods which should be applied to Property Blocks as a\n", - " whole, rather than individual elements of indexed Property Blocks.\n", - " \"\"\"\n", - "\n", - " def fix_initialization_states(self):\n", - " fix_state_vars(self)\n", - "\n", - "\n", - "@declare_process_block_class(\"AqPhaseStateBlock\", block_class=_AqueousStateBlock)\n", - "class AqPhaseStateBlockData(StateBlockData):\n", - " \"\"\"\n", - " An example property package for ideal gas properties with Gibbs energy\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction\n", - " \"\"\"\n", - " super().build()\n", - " self._make_state_vars()\n", - "\n", - " def _make_state_vars(self):\n", - " self.flow_vol = Var(\n", - " initialize=1,\n", - " domain=NonNegativeReals,\n", - " doc=\"Total volumetric flowrate\",\n", - " units=units.L / units.hour,\n", - " )\n", - "\n", - " self.conc_mass_comp = Var(\n", - " self.params.solute_set,\n", - " domain=NonNegativeReals,\n", - " initialize={\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1},\n", - " doc=\"Component mass concentrations\",\n", - " units=units.g / units.L,\n", - " )\n", - "\n", - " self.pressure = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=1,\n", - " bounds=(1, 5),\n", - " units=units.atm,\n", - " doc=\"State pressure [atm]\",\n", - " )\n", - "\n", - " self.temperature = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=300,\n", - " bounds=(273, 373),\n", - " units=units.K,\n", - " doc=\"State temperature [K]\",\n", - " )\n", - "\n", - " def material_flow_expression(self, j):\n", - " if j == \"H2O\":\n", - " return self.flow_vol * self.params.dens_mass\n", - " else:\n", - " return self.conc_mass_comp[j] * self.flow_vol\n", - "\n", - " self.material_flow_expression = Expression(\n", - " self.component_list,\n", - " rule=material_flow_expression,\n", - " doc=\"Material flow terms\",\n", - " )\n", - "\n", - " def enthalpy_flow_expression(self):\n", - " return (\n", - " self.flow_vol\n", - " * self.params.dens_mass\n", - " * self.params.cp_mass\n", - " * (self.temperature - self.params.temperature_ref)\n", - " )\n", - "\n", - " self.enthalpy_flow_expression = Expression(\n", - " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", - " )\n", - "\n", - " def get_flow_rate(self):\n", - " return self.flow_vol\n", - "\n", - " def get_material_flow_terms(self, p, j):\n", - " return self.material_flow_expression[j]\n", - "\n", - " def get_enthalpy_flow_terms(self, p):\n", - " return self.enthalpy_flow_expression\n", - "\n", - " def default_material_balance_type(self):\n", - " return MaterialBalanceType.componentTotal\n", - "\n", - " def default_energy_balance_type(self):\n", - " return EnergyBalanceType.enthalpyTotal\n", - "\n", - " def define_state_vars(self):\n", - " return {\n", - " \"flow_vol\": self.flow_vol,\n", - " \"conc_mass_comp\": self.conc_mass_comp,\n", - " \"temperature\": self.temperature,\n", - " \"pressure\": self.pressure,\n", - " }\n", - "\n", - " def get_material_flow_basis(self):\n", - " return MaterialFlowBasis.mass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 3. Liquid Liquid Extractor Unit Model\n", - "\n", - "Following the creation of property packages, our next step is to develop a unit model that facilitates the mass transfer of solutes between phases. This involves importing necessary libraries, building the unit model, defining auxiliary functions, and establishing the initialization routine for the unit model.\n", - "\n", - "## 3.1 Importing necessary libraries\n", - "\n", - "Let's commence by importing the essential libraries from Pyomo and IDAES." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Import Pyomo libraries\n", - "from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool\n", - "from pyomo.environ import (\n", - " value,\n", - " Constraint,\n", - " check_optimal_termination,\n", - ")\n", - "\n", - "# Import IDAES cores\n", - "from idaes.core import (\n", - " ControlVolume0DBlock,\n", - " declare_process_block_class,\n", - " MaterialBalanceType,\n", - " EnergyBalanceType,\n", - " MaterialFlowBasis,\n", - " MomentumBalanceType,\n", - " UnitModelBlockData,\n", - " useDefault,\n", - ")\n", - "from idaes.core.util.config import (\n", - " is_physical_parameter_block,\n", - " is_reaction_parameter_block,\n", - ")\n", - "\n", - "import idaes.logger as idaeslog\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.util.exceptions import ConfigurationError, InitializationError" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3.2 Creating the unit model\n", - "\n", - "Creating a unit model starts by creating a class called `LiqExtractionData` and use the `declare_process_block_class` decorator. The `LiqExtractionData` inherits the properties of `UnitModelBlockData` class, which allows us to create a control volume which is necessary for the unit model. After declaration of the class we proceed to define the relevant config arguments for the control volume. The config arguments includes the following properties:\n", - "\n", - "- `material_balance_type` - Indicates what type of mass balance should be constructed\n", - "- `has_pressure_change` - Indicates whether terms for pressure change should be\n", - "constructed\n", - "- `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", - "constructed\n", - "- `Organic Property` - Property parameter object used to define property calculations\n", - "for the Organic phase\n", - "- `Organic Property Arguments` - Arguments to use for constructing Organic phase properties\n", - "- `Aqueous Property` - Property parameter object used to define property calculations\n", - "for the aqueous phase\n", - "- `Aqueous Property Arguments` - Arguments to use for constructing aqueous phase properties\n", - "\n", - "As there are no pressure changes or reactions in this scenario, configuration arguments for these aspects are not included. However, additional details on configuration arguments can be found [here](https://github.com/IDAES/idaes-pse/blob/8948c6ce27d4c7f2c06b377a173f413599091998/idaes/models/unit_models/cstr.py)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@declare_process_block_class(\"LiqExtraction\")\n", - "class LiqExtractionData(UnitModelBlockData):\n", - " \"\"\"\n", - " LiqExtraction Unit Model Class\n", - " \"\"\"\n", - "\n", - " CONFIG = UnitModelBlockData.CONFIG()\n", - "\n", - " CONFIG.declare(\n", - " \"material_balance_type\",\n", - " ConfigValue(\n", - " default=MaterialBalanceType.useDefault,\n", - " domain=In(MaterialBalanceType),\n", - " description=\"Material balance construction flag\",\n", - " doc=\"\"\"Indicates what type of mass balance should be constructed,\n", - " **default** - MaterialBalanceType.useDefault.\n", - " **Valid values:** {\n", - " **MaterialBalanceType.useDefault - refer to property package for default\n", - " balance type\n", - " **MaterialBalanceType.none** - exclude material balances,\n", - " **MaterialBalanceType.componentPhase** - use phase component balances,\n", - " **MaterialBalanceType.componentTotal** - use total component balances,\n", - " **MaterialBalanceType.elementTotal** - use total element balances,\n", - " **MaterialBalanceType.total** - use total material balance.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_pressure_change\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Pressure change term construction flag\",\n", - " doc=\"\"\"Indicates whether terms for pressure change should be\n", - " constructed,\n", - " **default** - False.\n", - " **Valid values:** {\n", - " **True** - include pressure change terms,\n", - " **False** - exclude pressure change terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_phase_equilibrium\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Phase equilibrium construction flag\",\n", - " doc=\"\"\"Indicates whether terms for phase equilibrium should be\n", - " constructed,\n", - " **default** = False.\n", - " **Valid values:** {\n", - " **True** - include phase equilibrium terms\n", - " **False** - exclude phase equilibrium terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"organic_property_package\",\n", - " ConfigValue(\n", - " default=useDefault,\n", - " domain=is_physical_parameter_block,\n", - " description=\"Property package to use for organic phase\",\n", - " doc=\"\"\"Property parameter object used to define property calculations\n", - " for the organic phase,\n", - " **default** - useDefault.\n", - " **Valid values:** {\n", - " **useDefault** - use default package from parent model or flowsheet,\n", - " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"organic_property_package_args\",\n", - " ConfigBlock(\n", - " implicit=True,\n", - " description=\"Arguments to use for constructing organic phase properties\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to organic phase\n", - " property block(s) and used when constructing these,\n", - " **default** - None.\n", - " **Valid values:** {\n", - " see property package for documentation.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"aqueous_property_package\",\n", - " ConfigValue(\n", - " default=useDefault,\n", - " domain=is_physical_parameter_block,\n", - " description=\"Property package to use for aqueous phase\",\n", - " doc=\"\"\"Property parameter object used to define property calculations\n", - " for the aqueous phase,\n", - " **default** - useDefault.\n", - " **Valid values:** {\n", - " **useDefault** - use default package from parent model or flowsheet,\n", - " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"aqueous_property_package_args\",\n", - " ConfigBlock(\n", - " implicit=True,\n", - " description=\"Arguments to use for constructing aqueous phase properties\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to aqueous phase\n", - " property block(s) and used when constructing these,\n", - " **default** - None.\n", - " **Valid values:** {\n", - " see property package for documentation.}\"\"\",\n", - " ),\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Building the model\n", - "\n", - "After constructing the `LiqExtractionData` block and defining the config arguments for the control block, the next step is to write a build function that incorporates control volume and establishes constraints on the control volume to achieve the desired mass transfer. The control volume serves as a pivotal component in the unit model construction, representing the volume in which the process unfolds.\n", - "\n", - "IDAES provides flexibility in choosing control volumes based on geometry, with options including 0D or 1D. In this instance, we opt for a 0D control volume, the most commonly used control volume. This choice is suitable for systems where there is a well-mixed volume of fluid or where spatial variations are deemed negligible.\n", - "\n", - "The control volume encompasses parameters from (1-8), and its equations are configured to satisfy the specified config arguments. For a more in-depth understanding, users are encouraged to refer to [this resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst). \n", - "\n", - "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Org' for the Organic phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", - "\n", - "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the Organic phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, and the property package, along with property package arguments. \n", - "\n", - "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the Organic property package\n", - "\n", - "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", - "\n", - "$\\frac{\\partial M_{t, p, j}}{\\partial t} = F_{in, t, p, j} - F_{out, t, p, j} + N_{kinetic, t, p, j} + N_{equilibrium, t, p, j} + N_{pe, t, p, j} + N_{transfer, t, p, j} + N_{custom, t, p, j}$\n", - "\n", - "- $\\frac{\\partial M_{t, p, j}}{\\partial t}$ - Material accumulation\n", - "- $F_{in, t, p, j}$ - Flow into the control volume\n", - "- $F_{out, t, p, j}$ - Flow out of the control volume\n", - "- $N_{kinetic, t, p, j}$ - Rate of reaction generation\n", - "- $N_{equilibrium, t, p, j}$ - Equilibrium reaction generation\n", - "- $N_{pe, t, p, j}$ - Equilibrium reaction extent\n", - "- $N_{transfer, t, p, j}$ - Mass transfer\n", - "- $N_{custom, t, p, j}$ - User defined terms in material balance\n", - "\n", - "- t indicates time index\n", - "- p indicates phase index\n", - "- j indicates component index\n", - "- e indicates element index\n", - "- r indicates reaction name index\n", - "\n", - "Here we shall see that $N_{transfer, t, p, j}$ is the term in the equation which is responsible for the mass transfer and the `mass_transfer_term` should only be equal to the amount being transferred and not include a material balance on our own. For a detailed description of the terms one should refer to the following [resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst)\n", - "\n", - "This concludes the creation of organic phase control volume. Similar procedure is done for the aqueous phase control volume with aqueous property package. \n", - "\n", - "Now, the unit model has two control volumes with appropriate configurations and material, momentum and energy balances. The next step is to check the basis of the two property packages. They should both have the same flow basis, and an error is raised if this is not the case.\n", - "\n", - "Following this, the `add_inlet_ports` and `add_outlet_ports` functions are used to create inlet and outlet ports. These ports are named and assigned to each control volume, resulting in labeled inlet and outlet ports for each control volume.\n", - "\n", - "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the organic phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{org} = - mass\\_transfer\\_term_{aq} $\n", - "\n", - "Here $mass\\_transfer\\_term_{p}$ is the term indicating the amount of material being transferred from/to the phase and $D_{i}$ is the Distribution co-efficient for component i. \n", - "\n", - "This marks the completion of the build function, and the unit model is now equipped with the necessary process constraints. The subsequent steps involve writing the initialization routine." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def build(self):\n", - " \"\"\"\n", - " Begin building model (pre-DAE transformation).\n", - " Args:\n", - " None\n", - " Returns:\n", - " None\n", - " \"\"\"\n", - " # Call UnitModel.build to setup dynamics\n", - " super().build()\n", - "\n", - " # Check phase lists match assumptions\n", - " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the aquoues \"\n", - " f\"phase property package have a single phase named 'Aq'\"\n", - " )\n", - " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", - " f\"phase property package have a single phase named 'Org'\"\n", - " )\n", - "\n", - " # Check for at least one common component in component lists\n", - " if not any(\n", - " j in self.config.aqueous_property_package.component_list\n", - " for j in self.config.organic_property_package.component_list\n", - " ):\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", - " f\"and aqueous phase property packages have at least one \"\n", - " f\"common component.\"\n", - " )\n", - "\n", - " self.organic_phase = ControlVolume0DBlock(\n", - " dynamic=self.config.dynamic,\n", - " property_package=self.config.organic_property_package,\n", - " property_package_args=self.config.organic_property_package_args,\n", - " )\n", - "\n", - " self.organic_phase.add_state_blocks(\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium\n", - " )\n", - "\n", - " # Separate organic and aqueous phases means that phase equilibrium will\n", - " # be handled at the unit model level, thus has_phase_equilibrium is\n", - " # False, but has_mass_transfer is True.\n", - "\n", - " self.organic_phase.add_material_balances(\n", - " balance_type=self.config.material_balance_type,\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", - " has_mass_transfer=True,\n", - " )\n", - " # ---------------------------------------------------------------------\n", - "\n", - " self.aqueous_phase = ControlVolume0DBlock(\n", - " dynamic=self.config.dynamic,\n", - " property_package=self.config.aqueous_property_package,\n", - " property_package_args=self.config.aqueous_property_package_args,\n", - " )\n", - "\n", - " self.aqueous_phase.add_state_blocks(\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium\n", - " )\n", - "\n", - " # Separate liquid and aqueous phases means that phase equilibrium will\n", - " # be handled at the unit model level, thus has_phase_equilibrium is\n", - " # False, but has_mass_transfer is True.\n", - "\n", - " self.aqueous_phase.add_material_balances(\n", - " balance_type=self.config.material_balance_type,\n", - " # has_rate_reactions=False,\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", - " has_mass_transfer=True,\n", - " )\n", - "\n", - " self.aqueous_phase.add_geometry()\n", - "\n", - " # ---------------------------------------------------------------------\n", - " # Check flow basis is compatible\n", - " t_init = self.flowsheet().time.first()\n", - " if (\n", - " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", - " ):\n", - " raise ConfigurationError(\n", - " f\"{self.name} aqueous and organic property packages must use the \"\n", - " f\"same material flow basis.\"\n", - " )\n", - "\n", - " self.organic_phase.add_geometry()\n", - "\n", - " # Add Ports\n", - " self.add_inlet_port(\n", - " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", - " )\n", - " self.add_inlet_port(\n", - " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", - " )\n", - " self.add_outlet_port(\n", - " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", - " )\n", - " self.add_outlet_port(\n", - " name=\"aqueous_outlet\",\n", - " block=self.aqueous_phase,\n", - " doc=\"Aqueous outlet\",\n", - " )\n", - "\n", - " # ---------------------------------------------------------------------\n", - " # Add unit level constraints\n", - " # First, need the union and intersection of component lists\n", - " all_comps = (\n", - " self.aqueous_phase.properties_out.component_list\n", - " | self.organic_phase.properties_out.component_list\n", - " )\n", - " common_comps = (\n", - " self.aqueous_phase.properties_out.component_list\n", - " & self.organic_phase.properties_out.component_list\n", - " )\n", - "\n", - " # Get units for unit conversion\n", - " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", - " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", - " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - "\n", - " if flow_basis == MaterialFlowBasis.mass:\n", - " fb = \"flow_mass\"\n", - " elif flow_basis == MaterialFlowBasis.molar:\n", - " fb = \"flow_mole\"\n", - " else:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor only supports mass \"\n", - " f\"basis for MaterialFlowBasis.\"\n", - " )\n", - "\n", - " # Material balances\n", - " def rule_material_aq_balance(self, t, j):\n", - " if j in common_comps:\n", - " return self.aqueous_phase.mass_transfer_term[\n", - " t, \"Aq\", j\n", - " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", - " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", - " )\n", - " elif j in self.organic_phase.properties_out.component_list:\n", - " # No mass transfer term\n", - " # Set organic flowrate to an arbitrary small value\n", - " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", - " elif j in self.aqueous_phase.properties_out.component_list:\n", - " # No mass transfer term\n", - " # Set aqueous flowrate to an arbitrary small value\n", - " return self.aqueous_phase.mass_transfer_term[t, \"Aq\", j] == 0 * aunits(fb)\n", - "\n", - " self.material_aq_balance = Constraint(\n", - " self.flowsheet().time,\n", - " self.aqueous_phase.properties_out.component_list,\n", - " rule=rule_material_aq_balance,\n", - " doc=\"Unit level material balances for Aq\",\n", - " )\n", - "\n", - " def rule_material_liq_balance(self, t, j):\n", - " if j in common_comps:\n", - " return (\n", - " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", - " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", - " )\n", - " else:\n", - " # No mass transfer term\n", - " # Set organic flowrate to an arbitrary small value\n", - " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", - "\n", - " self.material_org_balance = Constraint(\n", - " self.flowsheet().time,\n", - " self.organic_phase.properties_out.component_list,\n", - " rule=rule_material_liq_balance,\n", - " doc=\"Unit level material balances Org\",\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Initialization Routine\n", - "\n", - "After writing the unit model it is crucial to initialize the model properly, as non-linear models may encounter local minima or infeasibility if not initialized properly. IDAES provides us with a few initialization routines which may not work for all the models, and in such cases the developer will have to define their own initialization routines. \n", - "\n", - "To create a custom initialization routine, model developers must create an initialize method as part of their model, and provide a sequence of steps intended to build up a feasible solution. Initialization routines generally make use of Pyomo’s tools for activating and deactivating constraints and often involve solving multiple sub-problems whilst building up an initial state.\n", - "\n", - "For this tutorial we would use the pre-defined initialization routine of `BlockTriangularizationInitializer` when initializing the model in the flowsheet. This Initializer should be suitable for most models, but may struggle to initialize\n", - "tightly coupled systems of equations. This method of initialization will follow the following workflow. \n", - "\n", - "- Have precheck for structural singularity\n", - "- Run incidence analysis on given block data and check matching.\n", - "- Call Block Triangularization solver on model.\n", - "- Call solve_strongly_connected_components on a given BlockData.\n", - "\n", - "For more details about this initialization routine can be found [here](https://github.com/IDAES/idaes-pse/blob/c09433b9afed5ae2fe25c0ccdc732783324f0101/idaes/core/initialization/block_triangularization.py). \n", - "\n", - "\n", - "This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_test.ipynb). The next sections will deal with the diagonistics and testing of the property package and unit model. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3.3 Building a Flowsheet\n", - "\n", - "Once we have set up the unit model and its property packages, we can start building a flowsheet using them. In this tutorial, we're focusing on a simple flowsheet with just a liquid-liquid extractor. To create the flowsheet we follow the following steps:\n", - "\n", - "- Import necessary libraries\n", - "- Create a Pyomo model.\n", - "- Inside the model, create a flowsheet block.\n", - "- Assign property packages to the flowsheet block.\n", - "- Add the liquid-liquid extractor to the flowsheet block.\n", - "- Fix variable to make it a square problem\n", - "- Run an initialization process.\n", - "- Solve the flowsheet.\n", - "\n", - "Following these steps, we've built a basic flowsheet using Pyomo. For more details, refer to the [documentation](../../flowsheets/hda_flowsheet_with_distillation_test.ipynb).\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pyomo.environ as pyo\n", - "import idaes.core\n", - "import idaes.models.unit_models\n", - "from idaes.core.solvers import get_solver\n", - "import idaes.logger as idaeslog\n", - "from pyomo.network import Arc\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.initialization import InitializationStatus\n", - "from idaes.core.initialization.block_triangularization import (\n", - " BlockTriangularizationInitializer,\n", - ")\n", - "from liquid_extraction.organic_property import OrgPhase\n", - "from liquid_extraction.aqueous_property import AqPhase\n", - "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", - "\n", - "\n", - "def build_model():\n", - " m = pyo.ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.lex = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - " return m\n", - "\n", - "\n", - "def fix_state_variables(m):\n", - " m.fs.lex.organic_inlet.flow_vol.fix(80 * pyo.units.L / pyo.units.hour)\n", - " m.fs.lex.organic_inlet.temperature.fix(300 * pyo.units.K)\n", - " m.fs.lex.organic_inlet.pressure.fix(1 * pyo.units.atm)\n", - " m.fs.lex.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", - " 1e-5 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", - " 1e-5 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", - " 1e-5 * pyo.units.g / pyo.units.L\n", - " )\n", - "\n", - " m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", - " m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", - " m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", - " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", - " 0.15 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", - " 0.2 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", - " 0.1 * pyo.units.g / pyo.units.L\n", - " )\n", - "\n", - " return m\n", - "\n", - "\n", - "def initialize_model(m):\n", - " initializer = BlockTriangularizationInitializer()\n", - " initializer.initialize(m.fs.lex)\n", - " return m\n", - "\n", - "\n", - "def main():\n", - " m = build_model()\n", - " m = fix_state_variables(m)\n", - " m = initialize_model(m)\n", - " return m\n", - "\n", - "\n", - "if __name__ == main:\n", - " main()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 4. Model Diagnostics using DiagnosticsToolbox\n", - "\n", - "Here, during initialization, we encounter warnings indicating that variables are being set to negative values, which is not expected behavior. These warnings suggest that there may be flaws in the model that require further investigation using the DiagnosticsToolbox from IDAES. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter_test.ipynb).\n", - "\n", - "To proceed with investigating these issues, we need to import the DiagnosticsToolbox. We can gain a better understanding of its functionality by running the help function on it. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from idaes.core.util import DiagnosticsToolbox" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The help() function provides comprehensive information on the DiagnosticsToolbox and all its supported methods. However, it's essential to focus on the initial steps outlined at the beginning of the docstring to get started effectively.\n", - "\n", - "Here's a breakdown of the steps to start with:\n", - "\n", - "- `Instantiate Model:` Ensure you have an instance of the model with a degrees of freedom equal to 0.\n", - "\n", - "- `Create DiagnosticsToolbox Instance:` Next, instantiate a DiagnosticsToolbox object.\n", - "\n", - "- `Provide Model to DiagnosticsToolbox:` Pass the model instance to the DiagnosticsToolbox.\n", - "\n", - "- `Call report_structural_issues() Function:` Finally, call the report_structural_issues() function. This function will highlight any warnings in the model's structure, such as unit inconsistencies or other issues related to variables in the caution section.\n", - "\n", - "By following these steps, you can efficiently utilize the DiagnosticsToolbox to identify and address any structural issues or warnings in your model." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "m = main()\n", - "dt = DiagnosticsToolbox(m)\n", - "dt.report_structural_issues()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Although no warnings were reported, it's important to note that there are 3 variables fixed to 0 and 10 unused variables, out of which 4 are fixed. As indicated in the output, the next step is to solve the model. After solving, you should call the report_numerical_issues() function. This function will help identify any numerical issues that may arise during the solution process." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "solver = pyo.SolverFactory(\"ipopt\")\n", - "solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The model is probably infeasible thus indicating numerical issues with the model. We should call the `report_numerical_issues()` function and check what the constraints/variables causing this issue. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dt.report_numerical_issues()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this scenario, it's observed that the condition number of the Jacobian is high, indicating that the Jacobian is ill-conditioned. Additionally, there are 2 warnings related to constraints with large residuals and variables at or outside the bounds. The cautions mentioned in the output are also related to these warnings.\n", - "\n", - "As suggested, the next steps would be to:\n", - "\n", - "- Call the `display_variables_at_or_outside_bounds()` function to investigate variables at or outside the bounds.\n", - "\n", - "- Call the `display_constraints_with_large_residuals()` function to examine constraints with large residuals.\n", - "\n", - "These steps will help identify the underlying causes of the numerical issues and constraints violations, allowing for further analysis and potential resolution. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dt.display_variables_at_or_outside_bounds()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this scenario, there are a couple of issues to address:\n", - "\n", - "- The pressure variable is fixed to 1, which is its lower bound. This could potentially lead to numerical issues, although it may not affect the model significantly since there is no pressure change in the model. To mitigate this, consider adjusting the lower bound of the pressure variable to avoid having its value at or outside the bounds.\n", - "\n", - "- The more concerning issue is with the `conc_mass_comp` variable attempting to go below 0 in the output. This suggests that there may be constraints involving `conc_mass_comp` in the aqueous phase causing this behavior. To investigate further, it's recommended to call the `display_constraints_with_large_residuals()` function. This will provide insights into whether constraints involving `conc_mass_comp` are contributing to the convergence issue." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dt.display_constraints_with_large_residuals()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As expected there are convergence issues with the constraints which have `conc_mass_comp` variable in them specifically in the aqeous phase. Now, let us investigate further by printing this constraints and checking the value of each term. Since this is an persistent issue across the components, we can focus on just one of the component to identify the issue. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", - "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", - "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", - "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", - "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It seems there is a discrepancy between the mass transfer term and the amount of input of NaCl. This can be inferred from the values where the input equals 15g/h and the `mass_transfer_term` equals -31.706g/h.\n", - "\n", - "To further investigate this issue, it's advisable to examine the `material_aq_balance` constraint within the unit model where the `mass_transfer_term` is defined. By printing out this constraint and analyzing its components, you can gain a better understanding of the discrepancy and take appropriate corrective actions." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here the problem can be tracked down easily as there being a typing error while recording the distribution factor. The distribution factor here was wrongly written ignoring its magnitude which should have been 1e-2, but that was missed, thus adjusting the distribution factor parameter we should have this issue resolved. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", - ")\n", - "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", - ")\n", - "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", - ")\n", - "\n", - "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", - "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "After the corrective actions, we should check if this have made any structural issues, for this we would call `report_structural_issues()`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dt.report_structural_issues()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now since there are no warnings we can go ahead and solve the model and see if the results are optimal. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is a good sign that the model solved optimally and a solution was found. \n", - "\n", - "**NOTE:** It is a good practice to run the model through DiagnosticsToolbox regardless of the solver termination status. \n", - "\n", - "The next section we shall focus on testing the unit model. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 5. Testing\n", - "\n", - "Testing is a crucial part of model development to ensure that the model works as expected, and remains reliable. Here's an overview of why we conduct testing:\n", - "\n", - "1. `Verify Correctness`: Testing ensure that the model works as expected and meets the specified requirements. \n", - "2. `Detect Bugs and Issues`: Testing helps in identifying bugs, errors, or unexpected behaviors in the code or model, allowing for timely fixes.\n", - "3. `Ensure Reliability`: Testing improves the reliability and robustness of the software, reducing the risk of failures when the user uses it.\n", - "4. `Support Changes`: Tests provide confidence when making changes or adding new features, ensuring that existing functionalities are not affected and work as they should.\n", - "\n", - "There are typically 3 types of tests:\n", - "\n", - "1. `Unit tests`: Test runs quickly (under 2 seconds) and has no network/system dependencies. Uses only libraries installed by default with the software\n", - "2. `Component test`: Test may run more slowly (under 10 seconds, or so), e.g. it may run a solver or create a bunch of files. Like unit tests, it still shouldn't depend on special libraries or dependencies.\n", - "3. `Integration test`: Test may take a long time to run, and may have complex dependencies.\n", - "\n", - "The expectation is that unit tests should be run by developers rather frequently, component tests should be run by the continuous integration system before running code, and integration tests are run across the codebase regularly, but infrequently (e.g. daily).\n", - "\n", - "\n", - "As a developer, testing is a crucial aspect of ensuring the reliability and correctness of the unit model. The testing process involves both Unit tests and Component tests, and pytest is used as the testing framework. A typical test is marked with @pytest.mark.level, where the level indicates the depth or specificity of the testing. This is written in a file usually named as test_*.py or *_test.py. The test files have functions written in them with the appropriate level of test being conducted. \n", - "\n", - "For more detailed information on testing methodologies and procedures, developers are encouraged to refer to [this resource](https://idaes-pse.readthedocs.io/en/stable/reference_guides/developer/testing.html). The resource provides comprehensive guidance on the testing process and ensures that the unit model meets the required standards and functionality.\n", - "\n", - "## 5.1 Property package\n", - "### Unit Tests\n", - "\n", - "When writing tests for the Aqueous property phase package, it's essential to focus on key aspects to ensure the correctness and robustness of the implementation. Here are the areas to cover in the unit tests:\n", - "\n", - "1. Number of Config Dictionaries: Verify that the property phase package has the expected number of configuration dictionaries.\n", - "\n", - "2. State Block Class Name: Confirm that the correct state block class is associated with the Aqueous property phase package.\n", - "\n", - "3. Number of Phases: Check that the Aqueous property phase package defines the expected number of phases.\n", - "\n", - "4. Components in the Phase and Physical Parameter Values: Test that the components present in the Aqueous phase match the anticipated list. Additionally, validate that the physical parameter values (such as density, viscosity, etc.) are correctly defined.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pytest\n", - "from pyomo.environ import ConcreteModel, Param, value, Var\n", - "from pyomo.util.check_units import assert_units_consistent\n", - "from idaes.core import MaterialBalanceType, EnergyBalanceType\n", - "\n", - "from liquid_extraction.organic_property import OrgPhase\n", - "from liquid_extraction.aqueous_property import AqPhase\n", - "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", - "from idaes.core.solvers import get_solver\n", - "\n", - "solver = get_solver()\n", - "\n", - "\n", - "class TestParamBlock(object):\n", - " @pytest.fixture(scope=\"class\")\n", - " def model(self):\n", - " model = ConcreteModel()\n", - " model.params = AqPhase()\n", - " return model\n", - "\n", - " @pytest.mark.unit\n", - " def test_config(self, model):\n", - " assert len(model.params.config) == 1\n", - "\n", - " @pytest.mark.unit\n", - " def test_build(self, model):\n", - " assert len(model.params.phase_list) == 1\n", - " for i in model.params.phase_list:\n", - " assert i == \"Aq\"\n", - "\n", - " assert len(model.params.component_list) == 4\n", - " for i in model.params.component_list:\n", - " assert i in [\"H2O\", \"NaCl\", \"KNO3\", \"CaSO4\"]\n", - "\n", - " assert isinstance(model.params.cp_mass, Param)\n", - " assert value(model.params.cp_mass) == 4182\n", - "\n", - " assert isinstance(model.params.dens_mass, Param)\n", - " assert value(model.params.dens_mass) == 997\n", - "\n", - " assert isinstance(model.params.temperature_ref, Param)\n", - " assert value(model.params.temperature_ref) == 298.15" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next set of unit tests focuses on testing the build function in the state block. Here are the key aspects to cover in these tests:\n", - "\n", - "1. Existence and Initialized Values of State Variables: Verify that the state variables are correctly defined and initialized within the state block. This ensures that the state block is properly constructed and ready for initialization.\n", - "\n", - "2. Initialization Function Test: Check that state variables are not fixed before initialization and are released after initialization. This test ensures that the initialization process occurs as expected and that the state variables are appropriately managed throughout.\n", - "\n", - "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the Aqueous property phase package. Similar tests can be written for the organic property package to ensure consistency and reliability across both packages." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class TestStateBlock(object):\n", - " @pytest.fixture(scope=\"class\")\n", - " def model(self):\n", - " model = ConcreteModel()\n", - " model.params = AqPhase()\n", - "\n", - " model.props = model.params.build_state_block([1])\n", - "\n", - " return model\n", - "\n", - " @pytest.mark.unit\n", - " def test_build(self, model):\n", - " assert isinstance(model.props[1].flow_vol, Var)\n", - " assert value(model.props[1].flow_vol) == 1\n", - "\n", - " assert isinstance(model.props[1].temperature, Var)\n", - " assert value(model.props[1].temperature) == 300\n", - "\n", - " assert isinstance(model.props[1].conc_mass_comp, Var)\n", - " assert len(model.props[1].conc_mass_comp) == 3\n", - "\n", - " @pytest.mark.unit\n", - " def test_initialize(self, model):\n", - " assert not model.props[1].flow_vol.fixed\n", - " assert not model.props[1].temperature.fixed\n", - " assert not model.props[1].pressure.fixed\n", - " for i in model.props[1].conc_mass_comp:\n", - " assert not model.props[1].conc_mass_comp[i].fixed\n", - "\n", - " model.props.initialize(hold_state=False, outlvl=1)\n", - "\n", - " assert not model.props[1].flow_vol.fixed\n", - " assert not model.props[1].temperature.fixed\n", - " assert not model.props[1].pressure.fixed\n", - " for i in model.props[1].conc_mass_comp:\n", - " assert not model.props[1].conc_mass_comp[i].fixed" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Component Tests\n", - "In the component test, we aim to ensure unit consistency across the entire property package. Unlike unit tests that focus on individual functions, component tests assess the coherence and consistency of the entire package. Here's what the component test will entail:\n", - "\n", - "Unit Consistency Check: Verify that all units used within the property package are consistent throughout. This involves checking that all parameters, variables, and equations within the package adhere to the same unit system, ensuring compatibility.\n", - "\n", - "By conducting a comprehensive component test, we can ensure that the property package functions as a cohesive unit, maintaining consistency and reliability across its entirety. This concludes our tests on the property package. Next we shall test the unit model. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "@pytest.mark.component\n", - "def check_units(model):\n", - " model = ConcreteModel()\n", - " model.params = AqPhase()\n", - " assert_units_consistent(model)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Testing the property package without the triggering pytest" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pyomo.util.check_units import assert_units_consistent\n", - "\n", - "m = ConcreteModel()\n", - "m.params = AqPhase()\n", - "m.props = m.params.build_state_block([1])\n", - "assert_units_consistent(m)\n", - "\n", - "assert len(m.props[1].conc_mass_comp) == 3" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Similar tests are done for the Organic Phase as well" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pyomo.util.check_units import assert_units_consistent\n", - "\n", - "m = ConcreteModel()\n", - "m.params = OrgPhase()\n", - "m.props = m.params.build_state_block([1])\n", - "assert_units_consistent(m)\n", - "\n", - "assert len(m.props[1].conc_mass_comp) == 3" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 5.2 Unit Model\n", - "### Unit tests\n", - "Unit tests for the unit model encompass verifying the configuration arguments and the build function, similar to the approach taken for the property package. When testing the config arguments, we ensure that the correct number of arguments is provided and then match each argument with the expected one. This ensures that the unit model is properly configured and ready to operate as intended." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import pytest\n", - "\n", - "import idaes.core\n", - "import idaes.models.unit_models\n", - "from idaes.core.solvers import get_solver\n", - "import idaes.logger as idaeslog\n", - "\n", - "\n", - "from pyomo.environ import value, check_optimal_termination, units\n", - "from pyomo.util.check_units import assert_units_consistent\n", - "from idaes.core.util.model_statistics import (\n", - " number_variables,\n", - " number_total_constraints,\n", - ")\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.initialization import (\n", - " SingleControlVolumeUnitInitializer,\n", - ")\n", - "\n", - "solver = get_solver()\n", - "\n", - "\n", - "@pytest.mark.unit\n", - "def test_config():\n", - " m = ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.unit = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - "\n", - " # Check unit config arguments\n", - " assert len(m.fs.unit.config) == 9\n", - "\n", - " # Check for config arguments\n", - " assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault\n", - " assert not m.fs.unit.config.has_pressure_change\n", - " assert not m.fs.unit.config.has_phase_equilibrium\n", - " assert m.fs.unit.config.organic_property_package is m.fs.org_properties\n", - " assert m.fs.unit.config.aqueous_property_package is m.fs.aq_properties\n", - "\n", - " # Check for unit initializer\n", - " assert m.fs.unit.default_initializer is SingleControlVolumeUnitInitializer" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Testing the config arguments for the flowsheet\n", - "test_config()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In testing the build function, we verify whether the number of variables aligns with the intended values and also check for the existence of desired constraints within the unit model. This ensures that the unit model is constructed accurately and includes all the necessary variables and constraints required for its proper functioning." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class TestBuild(object):\n", - " @pytest.fixture(scope=\"class\")\n", - " def model(self):\n", - " m = ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.unit = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - "\n", - " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.l / units.h)\n", - " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.l)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.l)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.l)\n", - "\n", - " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.l / units.h)\n", - " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.l)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.l)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.l)\n", - "\n", - " return m\n", - "\n", - " @pytest.mark.build\n", - " @pytest.mark.unit\n", - " def test_build(self, model):\n", - "\n", - " assert hasattr(model.fs.unit, \"aqueous_inlet\")\n", - " assert len(model.fs.unit.aqueous_inlet.vars) == 4\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"organic_inlet\")\n", - " assert len(model.fs.unit.organic_inlet.vars) == 4\n", - " assert hasattr(model.fs.unit.organic_inlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.organic_inlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.organic_inlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.organic_inlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"aqueous_outlet\")\n", - " assert len(model.fs.unit.aqueous_outlet.vars) == 4\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"organic_outlet\")\n", - " assert len(model.fs.unit.organic_outlet.vars) == 4\n", - " assert hasattr(model.fs.unit.organic_outlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.organic_outlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.organic_outlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.organic_outlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"material_aq_balance\")\n", - " assert hasattr(model.fs.unit, \"material_org_balance\")\n", - "\n", - " assert number_variables(model) == 34\n", - " assert number_total_constraints(model) == 16" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Component tests\n", - "\n", - "During the component tests, we evaluate the performance of the unit model when integrated with the property package. This evaluation process typically involves several steps:\n", - "\n", - "1. Unit Consistency Check: Verify that the unit model maintains consistency in its units throughout the model. This ensures that all variables and constraints within the model adhere to the same unit system, guaranteeing compatibility.\n", - "\n", - "2. Termination Condition Verification: This involves checking whether the model terminates optimally with the given inlet conditions.\n", - "\n", - "3. Variable Value Assessment: Check the values of outlet variables against the expected values. To account for the numerical tolerance of the solvers, the values are compared using the approx function with a relative tolerance.\n", - "\n", - "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered.\n", - "\n", - "5. Structural Issues: Verify that there are no structural issues with the model. \n", - "\n", - "By performing these checks, we conclude the testing for the unit model. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "class TestFlowsheet:\n", - " @pytest.fixture\n", - " def model(self):\n", - " m = ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.unit = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", - " )\n", - " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", - " )\n", - " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", - " )\n", - "\n", - " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", - " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", - "\n", - " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", - " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.kg)\n", - "\n", - " return m\n", - "\n", - " @pytest.mark.component\n", - " def test_unit_model(self, model):\n", - " assert_units_consistent(model)\n", - " solver = get_solver()\n", - " results = solver.solve(model, tee=False)\n", - "\n", - " # Check for optimal termination\n", - " assert check_optimal_termination(results)\n", - "\n", - " # Checking for outlet flows\n", - " assert value(model.fs.unit.organic_outlet.flow_vol[0]) == pytest.approx(\n", - " 80.0, rel=1e-5\n", - " )\n", - " assert value(model.fs.unit.aqueous_outlet.flow_vol[0]) == pytest.approx(\n", - " 10.0, rel=1e-5\n", - " )\n", - "\n", - " # Checking for outlet mass_comp\n", - " assert value(\n", - " model.fs.unit.organic_outlet.conc_mass_comp[0, \"CaSO4\"]\n", - " ) == pytest.approx(0.000187499, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.organic_outlet.conc_mass_comp[0, \"KNO3\"]\n", - " ) == pytest.approx(0.000749999, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.organic_outlet.conc_mass_comp[0, \"NaCl\"]\n", - " ) == pytest.approx(0.000403124, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"CaSO4\"]\n", - " ) == pytest.approx(0.0985, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"KNO3\"]\n", - " ) == pytest.approx(0.194, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"NaCl\"]\n", - " ) == pytest.approx(0.146775, rel=1e-5)\n", - "\n", - " # Checking for outlet temperature\n", - " assert value(model.fs.unit.organic_outlet.temperature[0]) == pytest.approx(\n", - " 300, rel=1e-5\n", - " )\n", - " assert value(model.fs.unit.aqueous_outlet.temperature[0]) == pytest.approx(\n", - " 300, rel=1e-5\n", - " )\n", - "\n", - " # Checking for outlet pressure\n", - " assert value(model.fs.unit.organic_outlet.pressure[0]) == pytest.approx(\n", - " 1, rel=1e-5\n", - " )\n", - " assert value(model.fs.unit.aqueous_outlet.pressure[0]) == pytest.approx(\n", - " 1, rel=1e-5\n", - " )\n", - "\n", - " # Fixed state variables\n", - " assert model.fs.unit.organic_inlet.flow_vol[0].fixed\n", - " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", - " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", - " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", - " assert model.fs.unit.organic_inlet.temperature[0].fixed\n", - " assert model.fs.unit.organic_inlet.pressure[0].fixed\n", - "\n", - " assert model.fs.unit.aqueous_inlet.flow_vol[0].fixed\n", - " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", - " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", - " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", - " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", - " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", - "\n", - " @pytest.mark.component\n", - " def test_structural_issues(self, model):\n", - " dt = DiagnosticsToolbox(model)\n", - " dt.assert_no_structural_warnings()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Testing the consolidated flowsheet. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from liquid_extraction.liq_liq_extractor_flowsheet import (\n", - " build_model,\n", - " fix_initial_state,\n", - " initialize_model,\n", - " solve_model,\n", - ")\n", - "from idaes.core import FlowsheetBlock\n", - "from idaes.core.util import DiagnosticsToolbox\n", - "\n", - "m = pyo.ConcreteModel(name=\"NGFC no CCS\")\n", - "m.fs = FlowsheetBlock(dynamic=False)\n", - "build_model(m)\n", - "fix_initial_state(m)\n", - "initialize_model(m)\n", - "solve_model(m)\n", - "\n", - "assert_units_consistent(m)\n", - "assert value(m.fs.lex.organic_outlet.temperature[0]) == pytest.approx(300, rel=1e-5)\n", - "dt = DiagnosticsToolbox(m)\n", - "dt.assert_no_numerical_warnings()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this tutorial, we have covered the comprehensive process of creating a custom unit model from scratch. Let's recap the key steps we have undertaken:\n", - "\n", - "- Developing property package\n", - "- Constructing the unit model \n", - "- Creating a Flowsheet\n", - "- Debugging the model using DiagnosticsToolbox\n", - "- Writing tests for the unit model\n", - "\n", - "By following the aforementioned procedure, one can create their own custom unit model. This would conclude the tutorial on creating custom unit model. " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "idaes-pse", - "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.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 3 -} + "cells": [ + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "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": [ + "# Creating Custom Unit Model\n", + "Author: Javal Vyas \n", + "Maintainer: Javal Vyas \n", + "\n", + "This tutorial is a comprehensive step-wise procedure to build a custom unit model from scratch. This tutorial will include creating a property package, a custom unit model and testing them. For this tutorial we shall create a custom unit model for Liquid - Liquid Extraction. \n", + "\n", + "The Liquid - Liquid Extractor model contains two immiscible fluids forming the two phases. One of the phases, say phase_1 has a high concentration of solutes which is to be separated. A mass transfer happens between the two phases and the solute is transferred from phase_1 to phase_2. This mass transfer is governed by a parameter called the distribution coefficient.\n", + "\n", + "After reviewing the working principles of the Liquid - Liquid Extractor, we shall proceed to create a custom unit model. We will require a property package for each phase, a custom unit model class and tests for the model and property packages.\n", + "\n", + "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", + "- Steady-state only\n", + "- Organic phase property package has a single phase named Org\n", + "- Aqueous phase property package has a single phase named Aq\n", + "- Organic and Aqueous phase properties need not have the same component list. \n", + "\n", + "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1. Creating Organic Property Package\n", + "\n", + "Creating a property package is a 4 step process\n", + "- Import necessary libraries \n", + "- Creating Physical Parameter Data Block\n", + "- Define State Block\n", + "- Define State Block Data\n", + "\n", + "# 1.1 Importing necessary packages \n", + "Let us begin with importing the necessary libraries where we will be using functionalities from IDAES and Pyomo. " + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Python libraries\n", + "import logging\n", + "\n", + "import idaes.logger as idaeslog\n", + "from idaes.core.util.initialization import fix_state_vars\n", + "\n", + "# Import Pyomo libraries\n", + "from pyomo.environ import (\n", + " Param,\n", + " Set,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", + "\n", + "# Import IDAES cores\n", + "from idaes.core import (\n", + " declare_process_block_class,\n", + " MaterialFlowBasis,\n", + " PhysicalParameterBlock,\n", + " StateBlockData,\n", + " StateBlock,\n", + " MaterialBalanceType,\n", + " EnergyBalanceType,\n", + " Solute,\n", + " Solvent,\n", + " LiquidPhase,\n", + ")\n", + "from idaes.core.util.model_statistics import degrees_of_freedom" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1.2 Physical Parameter Data Block\n", + "\n", + "A `PhysicalParameterBlock` serves as the central point of reference for all aspects of the property package and needs to define several things about the package. These are summarized below:\n", + "\n", + "- Units of measurement\n", + "- What properties are supported and how they are implemented\n", + "- What components and phases are included in the packages\n", + "- All the global parameters necessary for calculating properties\n", + "- A reference to the associated State Block class, so that construction of the State Block components can be automated from the Physical Parameter Block\n", + "\n", + "To construct this block, we begin by declaring a process block class using a Python decorator. One can learn more about `declare_process_block_class` [here](https://github.com/IDAES/idaes-pse/blob/eea1209077b75f7d940d8958362e69d4650c079d/idaes/core/base/process_block.py#L173). After constructing the process block, we define a build function which contains all the components that the property package would have. `super` function here is used to give access to methods and properties of a parent or sibling class and since this is used on the class `PhysicalParameterData` class, build has access to all the parent and sibling class methods. \n", + "\n", + "The `PhysicalParameterBlock` then refers to the `state block`, in this case `OrgPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. The solvent is in the Organic phase; we will assign the Phase as OrganicPhase, and the variable will be named Org as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", + " \n", + "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the Organic phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. Additionally, a parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", + "\n", + "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). In this method, we will call the predefined method `add_default_units()`.\n", + "\n", + "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default. " + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "@declare_process_block_class(\"OrgPhase\")\n", + "class PhysicalParameterData(PhysicalParameterBlock):\n", + " \"\"\"\n", + " Property Parameter Block Class\n", + "\n", + " Contains parameters and indexing sets associated with properties for\n", + " organic Phase\n", + "\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction.\n", + " \"\"\"\n", + " super().build()\n", + "\n", + " self._state_block_class = OrgPhaseStateBlock\n", + "\n", + " # List of valid phases in property package\n", + " self.Org = LiquidPhase()\n", + "\n", + " # Component list - a list of component identifiers\n", + " self.NaCl = Solute()\n", + " self.KNO3 = Solute()\n", + " self.CaSO4 = Solute()\n", + " self.solvent = (\n", + " Solvent()\n", + " ) # Solvent used here is ethylene dibromide (Organic Polar)\n", + "\n", + " # Heat capacity of solvent\n", + " self.cp_mass = Param(\n", + " mutable=True,\n", + " initialize=717.01,\n", + " doc=\"Specific heat capacity of solvent\",\n", + " units=units.J / units.kg / units.K,\n", + " )\n", + "\n", + " self.dens_mass = Param(\n", + " mutable=True,\n", + " initialize=2170,\n", + " doc=\"Density of ethylene dibromide\",\n", + " units=units.kg / units.m**3,\n", + " )\n", + " self.temperature_ref = Param(\n", + " within=PositiveReals,\n", + " mutable=True,\n", + " default=298.15,\n", + " doc=\"Reference temperature\",\n", + " units=units.K,\n", + " )\n", + " self.diffusion_factor = Param(\n", + " self.solute_set,\n", + " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", + " within=PositiveReals,\n", + " mutable=True,\n", + " )\n", + "\n", + " @classmethod\n", + " def define_metadata(cls, obj):\n", + " obj.add_default_units(\n", + " {\n", + " \"time\": units.hour,\n", + " \"length\": units.m,\n", + " \"mass\": units.g,\n", + " \"amount\": units.mol,\n", + " \"temperature\": units.K,\n", + " }\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1.3 State Block\n", + "\n", + "After the `PhysicalParameterBlock` class has been created, the next step is to write the code necessary to create the State Blocks that will be used throughout the flowsheet. `StateBlock` contains all the information necessary to define the state of the system. This includes the state variables and constraints on those variables which are used to describe a state property like the enthalpy, material balance, etc.\n", + "\n", + "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed StateBlock all at once (rather than element by element).\n", + "\n", + "The class `_OrganicStateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the `fix_initialization_states` function. `fix_initialization_states` function is used to fix the state variable within the state block with the provided initial values (usually inlet conditions). It takes a `block` as the argument in which the state variables are to be fixed. It also takes `state_args` as an optional argument. `state_args` is a dictionary with the value for the state variables to be fixed. This function returns a dictionary indexed by the block, state variables and variable index indicating the fixed status of each variable before applying the function. \n", + "\n", + "The above function comprise of the _OrganicStateBlock. Next, we shall see the construction of the OrgPhaseStateBlockData class." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "class _OrganicStateBlock(StateBlock):\n", + " \"\"\"\n", + " This Class contains methods which should be applied to Property Blocks as a\n", + " whole, rather than individual elements of indexed Property Blocks.\n", + " \"\"\"\n", + "\n", + " def fix_initialization_states(self):\n", + " fix_state_vars(self)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The class `OrgPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `OrgPhaseStateBlock`, and inherits the block class from `_OrganicStateBlock`. This inheritance allows `OrgPhaseStateBlockData` to leverage functions from `_OrganicStateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", + "\n", + "The subsequent objective is to delineate the state variables, accomplished through the `_make_state_vars` method. This method encompasses all the essential state variables and associated data. For this particular property package, the required state variables are:\n", + "\n", + "- `flow_vol` - volumetric flow rate\n", + "- `conc_mass_comp` - mass fractions\n", + "- `pressure` - state pressure\n", + "- `temperature` - state temperature\n", + "\n", + "After establishing the state variables, the subsequent step involves setting up state properties as constraints. This includes specifying the relationships and limitations that dictate the system's behavior. The following properties need to be articulated:\n", + "\n", + "-`get_material_flow_terms`: quantifies the amount of material flow.\n", + "- `get_enthalpy_flow_terms`: quantifies the amount of enthalpy flow.\n", + "- `get_flow_rate`: details volumetric flow rates.\n", + "- `default_material_balance_type`: defines the kind of material balance to be used.\n", + "- `default_energy_balance_type`: defines the kind of energy balance to be used.\n", + "- `define_state_vars`: involves defining state variables with units, akin to the define_metadata function in the PhysicalParameterData block.\n", + "- `get_material_flow_basis`: establishes the basis on which state variables are measured, whether in mass or molar terms.\n", + "\n", + "These definitions mark the conclusion of the state block construction and thus the property package. For additional details on creating a property package, please refer to this [resource](../../properties/custom/custom_physical_property_packages_test.ipynb ).\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_OrganicStateBlock)\n", + "class OrgPhaseStateBlockData(StateBlockData):\n", + " \"\"\"\n", + " An example property package for Organic phase for liquid liquid extraction\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction\n", + " \"\"\"\n", + " super().build()\n", + " self._make_state_vars()\n", + "\n", + " def _make_state_vars(self):\n", + " self.flow_vol = Var(\n", + " initialize=1,\n", + " domain=NonNegativeReals,\n", + " doc=\"Total volumetric flowrate\",\n", + " units=units.L / units.hour,\n", + " )\n", + " self.conc_mass_comp = Var(\n", + " self.params.solute_set,\n", + " domain=NonNegativeReals,\n", + " initialize=1,\n", + " doc=\"Component mass concentrations\",\n", + " units=units.g / units.L,\n", + " )\n", + " self.pressure = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=1,\n", + " bounds=(1, 5),\n", + " units=units.atm,\n", + " doc=\"State pressure [atm]\",\n", + " )\n", + "\n", + " self.temperature = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=300,\n", + " bounds=(273, 373),\n", + " units=units.K,\n", + " doc=\"State temperature [K]\",\n", + " )\n", + "\n", + " def material_flow_expression(self, j):\n", + " if j == \"solvent\":\n", + " return self.flow_vol * self.params.dens_mass\n", + " else:\n", + " return self.flow_vol * self.conc_mass_comp[j]\n", + "\n", + " self.material_flow_expression = Expression(\n", + " self.component_list,\n", + " rule=material_flow_expression,\n", + " doc=\"Material flow terms\",\n", + " )\n", + "\n", + " def enthalpy_flow_expression(self):\n", + " return (\n", + " self.flow_vol\n", + " * self.params.dens_mass\n", + " * self.params.cp_mass\n", + " * (self.temperature - self.params.temperature_ref)\n", + " )\n", + "\n", + " self.enthalpy_flow_expression = Expression(\n", + " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", + " )\n", + "\n", + " def get_flow_rate(self):\n", + " return self.flow_vol\n", + "\n", + " def get_material_flow_terms(self, p, j):\n", + " return self.material_flow_expression[j]\n", + "\n", + " def get_enthalpy_flow_terms(self, p):\n", + " return self.enthalpy_flow_expression\n", + "\n", + " def default_material_balance_type(self):\n", + " return MaterialBalanceType.componentTotal\n", + "\n", + " def default_energy_balance_type(self):\n", + " return EnergyBalanceType.enthalpyTotal\n", + "\n", + " def define_state_vars(self):\n", + " return {\n", + " \"flow_vol\": self.flow_vol,\n", + " \"conc_mass_comp\": self.conc_mass_comp,\n", + " \"temperature\": self.temperature,\n", + " \"pressure\": self.pressure,\n", + " }\n", + "\n", + " def get_material_flow_basis(self):\n", + " return MaterialFlowBasis.mass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 2. Creating Aqueous Property Package\n", + "\n", + "The structure of the Aqueous Property Package mirrors that of the Organic Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Python libraries\n", + "import logging\n", + "\n", + "from idaes.core.util.initialization import fix_state_vars\n", + "\n", + "# Import Pyomo libraries\n", + "from pyomo.environ import (\n", + " Param,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", + "\n", + "# Import IDAES cores\n", + "from idaes.core import (\n", + " declare_process_block_class,\n", + " MaterialFlowBasis,\n", + " PhysicalParameterBlock,\n", + " StateBlockData,\n", + " StateBlock,\n", + " MaterialBalanceType,\n", + " EnergyBalanceType,\n", + " Solute,\n", + " Solvent,\n", + " LiquidPhase,\n", + ")\n", + "\n", + "# Some more information about this module\n", + "__author__ = \"Javal Vyas\"\n", + "\n", + "\n", + "# Set up logger\n", + "_log = logging.getLogger(__name__)\n", + "\n", + "\n", + "@declare_process_block_class(\"AqPhase\")\n", + "class AqPhaseData(PhysicalParameterBlock):\n", + " \"\"\"\n", + " Property Parameter Block Class\n", + "\n", + " Contains parameters and indexing sets associated with properties for\n", + " aqueous Phase\n", + "\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction.\n", + " \"\"\"\n", + " super().build()\n", + "\n", + " self._state_block_class = AqPhaseStateBlock\n", + "\n", + " # List of valid phases in property package\n", + " self.Aq = LiquidPhase()\n", + "\n", + " # Component list - a list of component identifiers\n", + " self.NaCl = Solute()\n", + " self.KNO3 = Solute()\n", + " self.CaSO4 = Solute()\n", + " self.H2O = Solvent()\n", + "\n", + " # Heat capacity of solvent\n", + " self.cp_mass = Param(\n", + " mutable=True,\n", + " initialize=4182,\n", + " doc=\"Specific heat capacity of solvent\",\n", + " units=units.J / units.kg / units.K,\n", + " )\n", + "\n", + " self.dens_mass = Param(\n", + " mutable=True,\n", + " initialize=997,\n", + " doc=\"Density of ethylene dibromide\",\n", + " units=units.kg / units.m**3,\n", + " )\n", + " self.temperature_ref = Param(\n", + " within=PositiveReals,\n", + " mutable=True,\n", + " default=298.15,\n", + " doc=\"Reference temperature\",\n", + " units=units.K,\n", + " )\n", + "\n", + " @classmethod\n", + " def define_metadata(cls, obj):\n", + " obj.add_default_units(\n", + " {\n", + " \"time\": units.hour,\n", + " \"length\": units.m,\n", + " \"mass\": units.g,\n", + " \"amount\": units.mol,\n", + " \"temperature\": units.K,\n", + " }\n", + " )\n", + "\n", + "\n", + "class _AqueousStateBlock(StateBlock):\n", + " \"\"\"\n", + " This Class contains methods which should be applied to Property Blocks as a\n", + " whole, rather than individual elements of indexed Property Blocks.\n", + " \"\"\"\n", + "\n", + " def fix_initialization_states(self):\n", + " fix_state_vars(self)\n", + "\n", + "\n", + "@declare_process_block_class(\"AqPhaseStateBlock\", block_class=_AqueousStateBlock)\n", + "class AqPhaseStateBlockData(StateBlockData):\n", + " \"\"\"\n", + " An example property package for ideal gas properties with Gibbs energy\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction\n", + " \"\"\"\n", + " super().build()\n", + " self._make_state_vars()\n", + "\n", + " def _make_state_vars(self):\n", + " self.flow_vol = Var(\n", + " initialize=1,\n", + " domain=NonNegativeReals,\n", + " doc=\"Total volumetric flowrate\",\n", + " units=units.L / units.hour,\n", + " )\n", + "\n", + " self.conc_mass_comp = Var(\n", + " self.params.solute_set,\n", + " domain=NonNegativeReals,\n", + " initialize={\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1},\n", + " doc=\"Component mass concentrations\",\n", + " units=units.g / units.L,\n", + " )\n", + "\n", + " self.pressure = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=1,\n", + " bounds=(1, 5),\n", + " units=units.atm,\n", + " doc=\"State pressure [atm]\",\n", + " )\n", + "\n", + " self.temperature = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=300,\n", + " bounds=(273, 373),\n", + " units=units.K,\n", + " doc=\"State temperature [K]\",\n", + " )\n", + "\n", + " def material_flow_expression(self, j):\n", + " if j == \"H2O\":\n", + " return self.flow_vol * self.params.dens_mass\n", + " else:\n", + " return self.conc_mass_comp[j] * self.flow_vol\n", + "\n", + " self.material_flow_expression = Expression(\n", + " self.component_list,\n", + " rule=material_flow_expression,\n", + " doc=\"Material flow terms\",\n", + " )\n", + "\n", + " def enthalpy_flow_expression(self):\n", + " return (\n", + " self.flow_vol\n", + " * self.params.dens_mass\n", + " * self.params.cp_mass\n", + " * (self.temperature - self.params.temperature_ref)\n", + " )\n", + "\n", + " self.enthalpy_flow_expression = Expression(\n", + " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", + " )\n", + "\n", + " def get_flow_rate(self):\n", + " return self.flow_vol\n", + "\n", + " def get_material_flow_terms(self, p, j):\n", + " return self.material_flow_expression[j]\n", + "\n", + " def get_enthalpy_flow_terms(self, p):\n", + " return self.enthalpy_flow_expression\n", + "\n", + " def default_material_balance_type(self):\n", + " return MaterialBalanceType.componentTotal\n", + "\n", + " def default_energy_balance_type(self):\n", + " return EnergyBalanceType.enthalpyTotal\n", + "\n", + " def define_state_vars(self):\n", + " return {\n", + " \"flow_vol\": self.flow_vol,\n", + " \"conc_mass_comp\": self.conc_mass_comp,\n", + " \"temperature\": self.temperature,\n", + " \"pressure\": self.pressure,\n", + " }\n", + "\n", + " def get_material_flow_basis(self):\n", + " return MaterialFlowBasis.mass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 3. Liquid Liquid Extractor Unit Model\n", + "\n", + "Following the creation of property packages, our next step is to develop a unit model that facilitates the mass transfer of solutes between phases. This involves importing necessary libraries, building the unit model, defining auxiliary functions, and establishing the initialization routine for the unit model.\n", + "\n", + "## 3.1 Importing necessary libraries\n", + "\n", + "Let's commence by importing the essential libraries from Pyomo and IDAES." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Pyomo libraries\n", + "from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool\n", + "from pyomo.environ import (\n", + " value,\n", + " Constraint,\n", + " check_optimal_termination,\n", + ")\n", + "\n", + "# Import IDAES cores\n", + "from idaes.core import (\n", + " ControlVolume0DBlock,\n", + " declare_process_block_class,\n", + " MaterialBalanceType,\n", + " EnergyBalanceType,\n", + " MaterialFlowBasis,\n", + " MomentumBalanceType,\n", + " UnitModelBlockData,\n", + " useDefault,\n", + ")\n", + "from idaes.core.util.config import (\n", + " is_physical_parameter_block,\n", + " is_reaction_parameter_block,\n", + ")\n", + "\n", + "import idaes.logger as idaeslog\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.util.exceptions import ConfigurationError, InitializationError" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3.2 Creating the unit model\n", + "\n", + "Creating a unit model starts by creating a class called `LiqExtractionData` and using the `declare_process_block_class` decorator. The `LiqExtractionData` inherits the properties of the `UnitModelBlockData` class, which allows us to create a control volume that is necessary for the unit model. After declaration of the class we proceed to define the relevant config arguments for the control volume. The config arguments include the following properties:\n", + "\n", + "- `material_balance_type` - Indicates what type of mass balance should be constructed\n", + "- `has_pressure_change` - Indicates whether terms for pressure change should be\n", + "constructed\n", + "- `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", + "constructed\n", + "- `organic_property_package` - Property parameter object used to define property calculations\n", + "for the Organic phase\n", + "- `organic_property_package_args` - Arguments to use for constructing Organic phase properties\n", + "- `aqueous_property_package` - Property parameter object used to define property calculations\n", + "for the aqueous phase\n", + "- `aqueous_property_package_args` - Arguments to use for constructing aqueous phase properties\n", + "\n", + "As there are no pressure changes or reactions in this scenario, configuration arguments for these aspects are not included. However, additional details on configuration arguments can be found [here](https://github.com/IDAES/idaes-pse/blob/8948c6ce27d4c7f2c06b377a173f413599091998/idaes/models/unit_models/cstr.py)." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "@declare_process_block_class(\"LiqExtraction\")\n", + "class LiqExtractionData(UnitModelBlockData):\n", + " \"\"\"\n", + " LiqExtraction Unit Model Class\n", + " \"\"\"\n", + "\n", + " CONFIG = UnitModelBlockData.CONFIG()\n", + "\n", + " CONFIG.declare(\n", + " \"material_balance_type\",\n", + " ConfigValue(\n", + " default=MaterialBalanceType.useDefault,\n", + " domain=In(MaterialBalanceType),\n", + " description=\"Material balance construction flag\",\n", + " doc=\"\"\"Indicates what type of mass balance should be constructed,\n", + " **default** - MaterialBalanceType.useDefault.\n", + " **Valid values:** {\n", + " **MaterialBalanceType.useDefault - refer to property package for default\n", + " balance type\n", + " **MaterialBalanceType.none** - exclude material balances,\n", + " **MaterialBalanceType.componentPhase** - use phase component balances,\n", + " **MaterialBalanceType.componentTotal** - use total component balances,\n", + " **MaterialBalanceType.elementTotal** - use total element balances,\n", + " **MaterialBalanceType.total** - use total material balance.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"has_pressure_change\",\n", + " ConfigValue(\n", + " default=False,\n", + " domain=Bool,\n", + " description=\"Pressure change term construction flag\",\n", + " doc=\"\"\"Indicates whether terms for pressure change should be\n", + " constructed,\n", + " **default** - False.\n", + " **Valid values:** {\n", + " **True** - include pressure change terms,\n", + " **False** - exclude pressure change terms.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"has_phase_equilibrium\",\n", + " ConfigValue(\n", + " default=False,\n", + " domain=Bool,\n", + " description=\"Phase equilibrium construction flag\",\n", + " doc=\"\"\"Indicates whether terms for phase equilibrium should be\n", + " constructed,\n", + " **default** = False.\n", + " **Valid values:** {\n", + " **True** - include phase equilibrium terms\n", + " **False** - exclude phase equilibrium terms.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"organic_property_package\",\n", + " ConfigValue(\n", + " default=useDefault,\n", + " domain=is_physical_parameter_block,\n", + " description=\"Property package to use for organic phase\",\n", + " doc=\"\"\"Property parameter object used to define property calculations\n", + " for the organic phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"organic_property_package_args\",\n", + " ConfigBlock(\n", + " implicit=True,\n", + " description=\"Arguments to use for constructing organic phase properties\",\n", + " doc=\"\"\"A ConfigBlock with arguments to be passed to organic phase\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"aqueous_property_package\",\n", + " ConfigValue(\n", + " default=useDefault,\n", + " domain=is_physical_parameter_block,\n", + " description=\"Property package to use for aqueous phase\",\n", + " doc=\"\"\"Property parameter object used to define property calculations\n", + " for the aqueous phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"aqueous_property_package_args\",\n", + " ConfigBlock(\n", + " implicit=True,\n", + " description=\"Arguments to use for constructing aqueous phase properties\",\n", + " doc=\"\"\"A ConfigBlock with arguments to be passed to aqueous phase\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", + " ),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Building the model\n", + "\n", + "After constructing the `LiqExtractionData` block and defining the config arguments for the control block, the next step is to write a build function that incorporates the control volume and establishes constraints on the control volume to achieve the desired mass transfer. The control volume serves as a pivotal component in the unit model construction, representing the volume in which the process unfolds.\n", + "\n", + "IDAES provides flexibility in choosing control volumes based on geometry, with options including 0D or 1D. In this instance, we opt for a 0D control volume, the most commonly used control volume. This choice is suitable for systems where there is a well-mixed volume of fluid or where spatial variations are deemed negligible.\n", + "\n", + "The control volume encompasses parameters from (1-8), and its equations are configured to satisfy the specified config arguments. For a more in-depth understanding, users are encouraged to refer to [this resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst). \n", + "\n", + "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Org' for the organic phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", + "\n", + "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the organic phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, and the property package, along with property package arguments. \n", + "\n", + "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the organic property package\n", + "\n", + "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", + "\n", + "$\\frac{\\partial M_{t, p, j}}{\\partial t} = F_{in, t, p, j} - F_{out, t, p, j} + N_{kinetic, t, p, j} + N_{equilibrium, t, p, j} + N_{pe, t, p, j} + N_{transfer, t, p, j} + N_{custom, t, p, j}$\n", + "\n", + "- $\\frac{\\partial M_{t, p, j}}{\\partial t}$ - Material accumulation\n", + "- $F_{in, t, p, j}$ - Flow into the control volume\n", + "- $F_{out, t, p, j}$ - Flow out of the control volume\n", + "- $N_{kinetic, t, p, j}$ - Rate of reaction generation\n", + "- $N_{equilibrium, t, p, j}$ - Equilibrium reaction generation\n", + "- $N_{pe, t, p, j}$ - Equilibrium reaction extent\n", + "- $N_{transfer, t, p, j}$ - Mass transfer\n", + "- $N_{custom, t, p, j}$ - User defined terms in material balance\n", + "\n", + "- t indicates time index\n", + "- p indicates phase index\n", + "- j indicates component index\n", + "- e indicates element index\n", + "- r indicates reaction name index\n", + "\n", + "Here we shall see that $N_{transfer, t, p, j}$ is the term in the equation which is responsible for the mass transfer and the `mass_transfer_term` should only be equal to the amount being transferred and not include a material balance on our own. For a detailed description of the terms one should refer to the following [resource.](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst)\n", + "\n", + "This concludes the creation of the organic phase control volume. A similar procedure is done for the aqueous phase control volume with aqueous property package. \n", + "\n", + "Now, the unit model has two control volumes with appropriate configurations and material, momentum and energy balances. The next step is to check the basis of the two property packages. They should both have the same flow basis, and an error is raised if this is not the case.\n", + "\n", + "Following this, the `add_inlet_ports` and `add_outlet_ports` functions are used to create inlet and outlet ports. These ports are named and assigned to each control volume, resulting in labeled inlet and outlet ports for each control volume.\n", + "\n", + "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the organic phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{org} = - mass\\_transfer\\_term_{aq} $\n", + "\n", + "Here $mass\\_transfer\\_term_{p}$ is the term indicating the amount of material being transferred from/to the phase and $D_{i}$ is the Distribution coefficient for component i. \n", + "\n", + "This marks the completion of the build function, and the unit model is now equipped with the necessary process constraints. The subsequent steps involve writing the initialization routine." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "def build(self):\n", + " \"\"\"\n", + " Begin building model (pre-DAE transformation).\n", + " Args:\n", + " None\n", + " Returns:\n", + " None\n", + " \"\"\"\n", + " # Call UnitModel.build to setup dynamics\n", + " super().build()\n", + "\n", + " # Check phase lists match assumptions\n", + " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the aqueous \"\n", + " f\"phase property package have a single phase named 'Aq'\"\n", + " )\n", + " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"phase property package have a single phase named 'Org'\"\n", + " )\n", + "\n", + " # Check for at least one common component in component lists\n", + " if not any(\n", + " j in self.config.aqueous_property_package.component_list\n", + " for j in self.config.organic_property_package.component_list\n", + " ):\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"and aqueous phase property packages have at least one \"\n", + " f\"common component.\"\n", + " )\n", + "\n", + " self.organic_phase = ControlVolume0DBlock(\n", + " dynamic=self.config.dynamic,\n", + " property_package=self.config.organic_property_package,\n", + " property_package_args=self.config.organic_property_package_args,\n", + " )\n", + "\n", + " self.organic_phase.add_state_blocks(\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium\n", + " )\n", + "\n", + " # Separate organic and aqueous phases means that phase equilibrium will\n", + " # be handled at the unit model level, thus has_phase_equilibrium is\n", + " # False, but has_mass_transfer is True.\n", + "\n", + " self.organic_phase.add_material_balances(\n", + " balance_type=self.config.material_balance_type,\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", + " has_mass_transfer=True,\n", + " )\n", + " # ---------------------------------------------------------------------\n", + "\n", + " self.aqueous_phase = ControlVolume0DBlock(\n", + " dynamic=self.config.dynamic,\n", + " property_package=self.config.aqueous_property_package,\n", + " property_package_args=self.config.aqueous_property_package_args,\n", + " )\n", + "\n", + " self.aqueous_phase.add_state_blocks(\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium\n", + " )\n", + "\n", + " # Separate liquid and aqueous phases means that phase equilibrium will\n", + " # be handled at the unit model level, thus has_phase_equilibrium is\n", + " # False, but has_mass_transfer is True.\n", + "\n", + " self.aqueous_phase.add_material_balances(\n", + " balance_type=self.config.material_balance_type,\n", + " # has_rate_reactions=False,\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", + " has_mass_transfer=True,\n", + " )\n", + "\n", + " self.aqueous_phase.add_geometry()\n", + "\n", + " # ---------------------------------------------------------------------\n", + " # Check flow basis is compatible\n", + " t_init = self.flowsheet().time.first()\n", + " if (\n", + " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", + " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", + " ):\n", + " raise ConfigurationError(\n", + " f\"{self.name} aqueous and organic property packages must use the \"\n", + " f\"same material flow basis.\"\n", + " )\n", + "\n", + " self.organic_phase.add_geometry()\n", + "\n", + " # Add Ports\n", + " self.add_inlet_port(\n", + " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", + " )\n", + " self.add_inlet_port(\n", + " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", + " )\n", + " self.add_outlet_port(\n", + " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", + " )\n", + " self.add_outlet_port(\n", + " name=\"aqueous_outlet\",\n", + " block=self.aqueous_phase,\n", + " doc=\"Aqueous outlet\",\n", + " )\n", + "\n", + " # ---------------------------------------------------------------------\n", + " # Add unit level constraints\n", + " # First, need the union and intersection of component lists\n", + " all_comps = (\n", + " self.aqueous_phase.properties_out.component_list\n", + " | self.organic_phase.properties_out.component_list\n", + " )\n", + " common_comps = (\n", + " self.aqueous_phase.properties_out.component_list\n", + " & self.organic_phase.properties_out.component_list\n", + " )\n", + "\n", + " # Get units for unit conversion\n", + " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", + " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", + " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", + "\n", + " if flow_basis == MaterialFlowBasis.mass:\n", + " fb = \"flow_mass\"\n", + " else:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor only supports mass \"\n", + " f\"basis for MaterialFlowBasis.\"\n", + " )\n", + "\n", + " # Material balances\n", + " def rule_material_aq_balance(self, t, j):\n", + " if j in common_comps:\n", + " return self.aqueous_phase.mass_transfer_term[\n", + " t, \"Aq\", j\n", + " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", + " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", + " )\n", + " elif j in self.organic_phase.properties_out.component_list:\n", + " # No mass transfer term\n", + " # Set organic flowrate to an arbitrary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", + " elif j in self.aqueous_phase.properties_out.component_list:\n", + " # No mass transfer term\n", + " # Set aqueous flowrate to an arbitrary small value\n", + " return self.aqueous_phase.mass_transfer_term[t, \"Aq\", j] == 0 * aunits(fb)\n", + "\n", + " self.material_aq_balance = Constraint(\n", + " self.flowsheet().time,\n", + " self.aqueous_phase.properties_out.component_list,\n", + " rule=rule_material_aq_balance,\n", + " doc=\"Unit level material balances for Aq\",\n", + " )\n", + "\n", + " def rule_material_liq_balance(self, t, j):\n", + " if j in common_comps:\n", + " return (\n", + " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", + " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", + " )\n", + " else:\n", + " # No mass transfer term\n", + " # Set organic flowrate to an arbitrary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", + "\n", + " self.material_org_balance = Constraint(\n", + " self.flowsheet().time,\n", + " self.organic_phase.properties_out.component_list,\n", + " rule=rule_material_liq_balance,\n", + " doc=\"Unit level material balances Org\",\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialization Routine\n", + "\n", + "After writing the unit model it is crucial to initialize the model properly, as non-linear models may encounter local minima or infeasibility if not initialized properly. IDAES provides us with a few initialization routines which may not work for all the models, and in such cases the developer will have to define their own initialization routines. \n", + "\n", + "To create a custom initialization routine, model developers must create an initialize method as part of their model, and provide a sequence of steps intended to build up a feasible solution. Initialization routines generally make use of Pyomo\u2019s tools for activating and deactivating constraints and often involve solving multiple sub-problems whilst building up an initial state.\n", + "\n", + "For this tutorial we would use the pre-defined initialization routine of `BlockTriangularizationInitializer` when initializing the model in the flowsheet. This Initializer should be suitable for most models, but may struggle to initialize\n", + "tightly coupled systems of equations. This method of initialization will follow the following workflow. \n", + "\n", + "- Have precheck for structural singularity\n", + "- Run incidence analysis on given block data and check matching.\n", + "- Call Block Triangularization solver on the model.\n", + "- Call solve_strongly_connected_components on a given BlockData.\n", + "\n", + "More details about this initialization routine can be found [here](https://github.com/IDAES/idaes-pse/blob/c09433b9afed5ae2fe25c0ccdc732783324f0101/idaes/core/initialization/block_triangularization.py). \n", + "\n", + "\n", + "This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_test.ipynb). The next sections will deal with the diagnostics and testing of the property package and unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3.3 Building a Flowsheet\n", + "\n", + "Once we have set up the unit model and its property packages, we can start building a flowsheet using them. In this tutorial, we're focusing on a simple flowsheet with just a liquid-liquid extractor. To create the flowsheet we follow the following steps:\n", + "\n", + "- Import necessary libraries\n", + "- Create a Pyomo model.\n", + "- Inside the model, create a flowsheet block.\n", + "- Assign property packages to the flowsheet block.\n", + "- Add the liquid-liquid extractor to the flowsheet block.\n", + "- Fix variable to make it a square problem\n", + "- Run an initialization process.\n", + "- Solve the flowsheet.\n", + "\n", + "Following these steps, we've built a basic flowsheet using Pyomo. For more details, refer to the [documentation](../../flowsheets/hda_flowsheet_with_distillation_test.ipynb).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "Ipopt 3.13.2: linear_solver=\"ma57\"\n", + "max_iter=200\n", + "nlp_scaling_method=\"gradient-based\"\n", + "tol=1e-08\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...: 6\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 0\n", + "\n", + "Total number of variables............................: 6\n", + " variables with only lower bounds: 6\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.98e+01 -1.0 4.10e-01 - 1.00e+00 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.05e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.97e+04 1.6 3.67e+02 - 1.00e+00 2.30e-03f 1\n", + " 5r 0.0000000e+00 4.00e+01 1.13e+02 1.6 3.89e-01 - 9.97e-01 1.00e+00f 1\n", + " 6r 0.0000000e+00 4.00e+01 1.13e+00 -1.2 8.42e-02 - 9.92e-01 9.87e-01f 1\n", + " 7r 0.0000000e+00 4.00e+01 9.78e-04 -4.3 8.94e-04 - 1.00e+00 1.00e+00f 1\n", + " 8r 0.0000000e+00 4.00e+01 5.79e-05 -6.4 1.49e-07 - 1.00e+00 1.00e+00f 1\n", + " 9r 0.0000000e+00 4.00e+01 5.20e-06 -9.0 6.12e-10 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 9\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 2.5739427655935287e-07 2.5739427655935287e-07\n", + "Constraint violation....: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "Complementarity.........: 9.0909090910996662e-10 9.0909090910996662e-10\n", + "Overall NLP error.......: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 15\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 15\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 12\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "Could not solve fs.lex after block triangularization finished.\n" + ] + } + ], + "source": [ + "import pyomo.environ as pyo\n", + "from idaes.core import FlowsheetBlock\n", + "\n", + "from idaes.core.initialization.block_triangularization import (\n", + " BlockTriangularizationInitializer,\n", + ")\n", + "from liquid_extraction.organic_property import OrgPhase\n", + "from liquid_extraction.aqueous_property import AqPhase\n", + "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", + "\n", + "\n", + "def build_model():\n", + " m = pyo.ConcreteModel()\n", + " m.fs = FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.lex = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + " return m\n", + "\n", + "\n", + "def fix_state_variables(m):\n", + " m.fs.lex.organic_inlet.flow_vol.fix(80 * pyo.units.L / pyo.units.hour)\n", + " m.fs.lex.organic_inlet.temperature.fix(300 * pyo.units.K)\n", + " m.fs.lex.organic_inlet.pressure.fix(1 * pyo.units.atm)\n", + " m.fs.lex.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", + " 1e-5 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", + " 1e-5 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", + " 1e-5 * pyo.units.g / pyo.units.L\n", + " )\n", + "\n", + " m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", + " m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", + " m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", + " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", + " 0.15 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", + " 0.2 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", + " 0.1 * pyo.units.g / pyo.units.L\n", + " )\n", + "\n", + " return m\n", + "\n", + "\n", + "def initialize_model(m):\n", + " initializer = BlockTriangularizationInitializer()\n", + " try:\n", + " initializer.initialize(m.fs.lex)\n", + " except InitializationError as err:\n", + " print(err)\n", + " return m\n", + "\n", + "\n", + "def main():\n", + " m = build_model()\n", + " m = fix_state_variables(m)\n", + " m = initialize_model(m)\n", + " return m\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4. Model Diagnostics using DiagnosticsToolbox\n", + "\n", + "Here, during initialization, we encounter warnings indicating that variables are being set to negative values before an exception is raised stating that solving the model failed. These issues suggest that there may be flaws in the model that require further investigation using the DiagnosticsToolbox from IDAES. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter_test.ipynb).\n", + "\n", + "To proceed with investigating these issues, we need to import the DiagnosticsToolbox. We can gain a better understanding of its functionality by running the help function on it. " + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "from idaes.core.util import DiagnosticsToolbox" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The help() function provides comprehensive information on the DiagnosticsToolbox and all its supported methods. However, it's essential to focus on the initial steps outlined at the beginning of the docstring to get started effectively.\n", + "\n", + "Here's a breakdown of the steps to start with:\n", + "\n", + "- `Instantiate Model:` Ensure you have an instance of the model with degrees of freedom equal to 0.\n", + "\n", + "- `Create DiagnosticsToolbox Instance:` Next, instantiate a DiagnosticsToolbox object.\n", + "\n", + "- `Provide Model to DiagnosticsToolbox:` Pass the model instance to the DiagnosticsToolbox.\n", + "\n", + "- `Call report_structural_issues() Function:` Finally, call the report_structural_issues() function. This function will highlight any warnings in the model's structure, such as unit inconsistencies or other issues related to variables in the caution section.\n", + "\n", + "By following these steps, you can efficiently utilize the DiagnosticsToolbox to identify and address any structural issues or warnings in your model." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "Ipopt 3.13.2: linear_solver=\"ma57\"\n", + "max_iter=200\n", + "nlp_scaling_method=\"gradient-based\"\n", + "tol=1e-08\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...: 6\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 0\n", + "\n", + "Total number of variables............................: 6\n", + " variables with only lower bounds: 6\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.98e+01 -1.0 4.10e-01 - 1.00e+00 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.05e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.97e+04 1.6 3.67e+02 - 1.00e+00 2.30e-03f 1\n", + " 5r 0.0000000e+00 4.00e+01 1.13e+02 1.6 3.89e-01 - 9.97e-01 1.00e+00f 1\n", + " 6r 0.0000000e+00 4.00e+01 1.13e+00 -1.2 8.42e-02 - 9.92e-01 9.87e-01f 1\n", + " 7r 0.0000000e+00 4.00e+01 9.78e-04 -4.3 8.94e-04 - 1.00e+00 1.00e+00f 1\n", + " 8r 0.0000000e+00 4.00e+01 5.79e-05 -6.4 1.49e-07 - 1.00e+00 1.00e+00f 1\n", + " 9r 0.0000000e+00 4.00e+01 5.20e-06 -9.0 6.12e-10 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 9\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 2.5739427655935287e-07 2.5739427655935287e-07\n", + "Constraint violation....: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "Complementarity.........: 9.0909090910996662e-10 9.0909090910996662e-10\n", + "Overall NLP error.......: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 15\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 15\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 12\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "Could not solve fs.lex after block triangularization finished.\n", + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "1 Cautions\n", + "\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "m = main()\n", + "dt = DiagnosticsToolbox(m)\n", + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Although no warnings were reported, it's important to note that there are 3 variables fixed to 0 and 10 unused variables, out of which 4 are fixed. As indicated in the output, the next step is to solve the model. After solving, you should call the report_numerical_issues() function. This function will help identify any numerical issues that may arise during the solution process." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "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...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.93e+01 -1.0 4.10e-01 - 9.91e-01 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.03e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.88e+04 1.6 3.68e+02 - 9.92e-01 2.29e-03f 1\n", + " 5r 0.0000000e+00 3.60e+01 3.03e+00 1.6 4.01e+00 - 1.00e+00 1.00e+00f 1\n", + " 6r 0.0000000e+00 3.69e+01 1.21e+01 -1.2 9.24e-01 - 9.69e-01 9.78e-01f 1\n", + " 7r 0.0000000e+00 3.70e+01 2.11e-01 -1.9 1.00e-01 - 9.97e-01 1.00e+00f 1\n", + " 8r 0.0000000e+00 3.78e+01 2.03e-02 -4.3 8.71e-01 - 9.71e-01 1.00e+00f 1\n", + " 9r 0.0000000e+00 3.80e+01 2.62e-04 -6.4 1.24e-01 - 9.99e-01 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10r 0.0000000e+00 3.81e+01 5.87e-09 -6.4 1.58e-01 - 1.00e+00 1.00e+00f 1\n", + " 11r 0.0000000e+00 3.91e+01 1.09e-05 -9.0 9.35e-01 - 9.68e-01 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 11\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 5.1393961893966849e-07 5.1393961893966849e-07\n", + "Constraint violation....: 3.9105165554489545e+01 3.9105165554489545e+01\n", + "Complementarity.........: 9.0909090910996620e-10 9.0909090910996620e-10\n", + "Overall NLP error.......: 3.9105165554489545e+01 3.9105165554489545e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 17\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 17\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 14\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "WARNING: Loading a SolverResults object with a warning status into\n", + "model.name=\"unknown\";\n", + " - termination condition: infeasible\n", + " - message from solver: Ipopt 3.13.2\\x3a Converged to a locally infeasible\n", + " point. Problem may be infeasible.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06122612953186035}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver = pyo.SolverFactory(\"ipopt\")\n", + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model is probably infeasible, indicating numerical issues with the model. We should call the `report_numerical_issues()` function and check the constraints/variables causing this issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Jacobian Condition Number: 7.955E+03\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 WARNINGS\n", + "\n", + " WARNING: 6 Constraints with large residuals (>1.0E-05)\n", + " WARNING: 5 Variables at or outside bounds (tol=0.0E+00)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "3 Cautions\n", + "\n", + " Caution: 8 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", + " Caution: 5 Variables with value close to zero (tol=1.0E-08)\n", + " Caution: 3 Variables with extreme value (<1.0E-04 or >1.0E+04)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " display_constraints_with_large_residuals()\n", + " compute_infeasibility_explanation()\n", + " display_variables_at_or_outside_bounds()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_numerical_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, it's observed that the condition number of the Jacobian is high, indicating that the Jacobian is ill-conditioned. Additionally, there are 2 warnings related to constraints with large residuals and variables at or outside the bounds. The cautions mentioned in the output are also related to these warnings.\n", + "\n", + "As suggested, the next steps would be to:\n", + "\n", + "- Call the `display_variables_at_or_outside_bounds()` function to investigate variables at or outside the bounds.\n", + "\n", + "- Call the `display_constraints_with_large_residuals()` function to examine constraints with large residuals.\n", + "\n", + "These steps will help identify the underlying causes of the numerical issues and constraints violations, allowing for further analysis and potential resolution. " + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following variable(s) have values at or outside their bounds (tol=0.0E+00):\n", + "\n", + " fs.lex.organic_phase.properties_in[0.0].pressure (fixed): value=1.0 bounds=(1, 5)\n", + " fs.lex.organic_phase.properties_out[0.0].pressure (free): value=1 bounds=(1, 5)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4] (free): value=0.0 bounds=(0, None)\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_variables_at_or_outside_bounds()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, there are a couple of issues to address:\n", + "\n", + "- The pressure variable is fixed to 1, which is its lower bound. This could potentially lead to numerical issues, although it may not affect the model significantly since there is no pressure change in the model. To mitigate this, consider adjusting the lower bound of the pressure variable to avoid having its value at or outside the bounds.\n", + "\n", + "- The more concerning issue is with the `conc_mass_comp` variable attempting to go below 0 in the output. This suggests that there may be constraints involving `conc_mass_comp` in the aqueous phase causing this behavior. To investigate further, it's recommended to call the `display_constraints_with_large_residuals()` function. This will provide insights into whether constraints involving `conc_mass_comp` are contributing to the convergence issue." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following constraint(s) have large residuals (>1.0E-05):\n", + "\n", + " fs.lex.material_aq_balance[0.0,NaCl]: 5.49716E-01\n", + " fs.lex.material_aq_balance[0.0,KNO3]: 8.94833E-01\n", + " fs.lex.material_aq_balance[0.0,CaSO4]: 5.48843E-02\n", + " fs.lex.aqueous_phase.material_balances[0.0,NaCl]: 1.67003E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,KNO3]: 3.91052E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,CaSO4]: 4.94512E+00\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_constraints_with_large_residuals()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected there are convergence issues with the constraints which have `conc_mass_comp` variable in them specifically in the aqueous phase. Now, let us investigate further by printing this constraints and checking the value of each term. Since this is an persistent issue across the components, we can focus on just one of the component to identify the issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_balances} : Material balances\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : (fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) - (fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_out[0.0].flow_vol) + fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solute_set, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.15 : None : True : True : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : True : True : NonNegativeReals\n", + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solute_set, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.0 : None : False : False : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : False : False : NonNegativeReals\n", + "{Member of mass_transfer_term} : Component material transfer into unit\n", + " Size=4, Index=fs._time*fs.aq_properties._phase_component_set, Units=g/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " (0.0, 'Aq', 'NaCl') : None : -31.700284300098897 : None : False : False : Reals\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems there is a discrepancy between the mass transfer term and the amount of input of NaCl. This can be inferred from the values where the input equals 15g/h and the `mass_transfer_term` equals -31.706g/h.\n", + "\n", + "To further investigate this issue, it's advisable to examine the `material_aq_balance` constraint within the unit model where the `mass_transfer_term` is defined. By printing out this constraint and analyzing its components, you can gain a better understanding of the discrepancy and take appropriate corrective actions." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_aq_balance} : Unit level material balances for Aq\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] + fs.org_properties.diffusion_factor[NaCl]*(fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here the problem can be tracked down easily as there being a typing error while recording the distribution factor. The distribution factor here was wrongly written ignoring its magnitude which should have been 1e-2, but that was missed, thus adjusting the distribution factor parameter we should have this issue resolved. " + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + ")\n", + "\n", + "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", + "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the corrective actions, we should check if this has made any structural issues, for this we would call `report_structural_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "1 Cautions\n", + "\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now since there are no warnings we can go ahead and solve the model and see if the results are optimal. " + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "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...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\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 5.85e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 3.55e-15 8.41e+00 -1.0 5.85e+01 - 1.05e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 1\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 3.5527136788005009e-15 3.5527136788005009e-15\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 3.5527136788005009e-15 3.5527136788005009e-15\n", + "\n", + "\n", + "Number of objective function evaluations = 2\n", + "Number of objective gradient evaluations = 2\n", + "Number of equality constraint evaluations = 2\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 2\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 1\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.0668952465057373}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a good sign that the model solved optimally and a solution was found. \n", + "\n", + "**NOTE:** It is a good practice to run the model through DiagnosticsToolbox regardless of the solver termination status. \n", + "\n", + "The next section we shall focus on testing the unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5. Testing\n", + "\n", + "Testing is a crucial part of model development to ensure that the model works as expected, and remains reliable. Here's an overview of why we conduct testing:\n", + "\n", + "1. `Verify Correctness`: Testing ensures that the model works as expected and meets the specified requirements. \n", + "2. `Detect Bugs and Issues`: Testing helps in identifying bugs, errors, or unexpected behaviors in the code or model, allowing for timely fixes.\n", + "3. `Ensure Reliability`: Testing improves the reliability and robustness of the software, reducing the risk of failures when the user uses it.\n", + "4. `Support Changes`: Tests provide confidence when making changes or adding new features, ensuring that existing functionalities are not affected and work as they should.\n", + "\n", + "There are typically 3 types of tests:\n", + "\n", + "1. `Unit tests`: Test runs quickly (under 2 seconds) and has no network/system dependencies. Uses only libraries installed by default with the software\n", + "2. `Component test`: Test may run more slowly (under 10 seconds, or so), e.g. it may run a solver or create a bunch of files. Like unit tests, it still shouldn't depend on special libraries or dependencies.\n", + "3. `Integration test`: Test may take a long time to run, and may have complex dependencies.\n", + "\n", + "The expectation is that unit tests should be run by developers rather frequently, component tests should be run by the continuous integration system before running code, and integration tests are run across the codebase regularly, but infrequently (e.g. daily).\n", + "\n", + "\n", + "As a developer, testing is a crucial aspect of ensuring the reliability and correctness of the unit model. The testing process involves both Unit tests and Component tests, and pytest is used as the testing framework. A typical test is marked with @pytest.mark.level, where the level indicates the depth or specificity of the testing. This is written in a file usually named as test_*.py or *_test.py. The test files have functions written in them with the appropriate level of test being conducted. \n", + "\n", + "For more detailed information on testing methodologies and procedures, developers are encouraged to refer to [this resource](https://idaes-pse.readthedocs.io/en/stable/reference_guides/developer/testing.html). The resource provides comprehensive guidance on the testing process and ensures that the unit model meets the required standards and functionality.\n", + "\n", + "## 5.1 Property package\n", + "### Unit Tests\n", + "\n", + "When writing tests for the Aqueous property phase package, it's essential to focus on key aspects to ensure the correctness and robustness of the implementation. Here are the areas to cover in the unit tests:\n", + "\n", + "1. Number of Config Dictionaries: Verify that the property phase package has the expected number of configuration dictionaries.\n", + "\n", + "2. State Block Class Name: Confirm that the correct state block class is associated with the Aqueous property phase package.\n", + "\n", + "3. Number of Phases: Check that the Aqueous property phase package defines the expected number of phases.\n", + "\n", + "4. Components in the Phase and Physical Parameter Values: Test that the components present in the Aqueous phase match the anticipated list. Additionally, validate that the physical parameter values (such as density, viscosity, etc.) are correctly defined.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "import pytest\n", + "from pyomo.environ import ConcreteModel, Param, value, Var\n", + "from pyomo.util.check_units import assert_units_consistent\n", + "from idaes.core import MaterialBalanceType, EnergyBalanceType\n", + "\n", + "from liquid_extraction.organic_property import OrgPhase\n", + "from liquid_extraction.aqueous_property import AqPhase\n", + "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", + "from idaes.core.solvers import get_solver\n", + "\n", + "solver = get_solver()\n", + "\n", + "\n", + "class TestParamBlock(object):\n", + " @pytest.fixture(scope=\"class\")\n", + " def model(self):\n", + " model = ConcreteModel()\n", + " model.params = AqPhase()\n", + " return model\n", + "\n", + " @pytest.mark.unit\n", + " def test_config(self, model):\n", + " assert len(model.params.config) == 1\n", + "\n", + " @pytest.mark.unit\n", + " def test_build(self, model):\n", + " assert len(model.params.phase_list) == 1\n", + " for i in model.params.phase_list:\n", + " assert i == \"Aq\"\n", + "\n", + " assert len(model.params.component_list) == 4\n", + " for i in model.params.component_list:\n", + " assert i in [\"H2O\", \"NaCl\", \"KNO3\", \"CaSO4\"]\n", + "\n", + " assert isinstance(model.params.cp_mass, Param)\n", + " assert value(model.params.cp_mass) == 4182\n", + "\n", + " assert isinstance(model.params.dens_mass, Param)\n", + " assert value(model.params.dens_mass) == 997\n", + "\n", + " assert isinstance(model.params.temperature_ref, Param)\n", + " assert value(model.params.temperature_ref) == 298.15" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next set of unit tests focuses on testing the build function in the state block. Here are the key aspects to cover in these tests:\n", + "\n", + "1. Existence and Initialized Values of State Variables: Verify that the state variables are correctly defined and initialized within the state block. This ensures that the state block is properly constructed and ready for initialization.\n", + "\n", + "2. Initialization Function Test: Check that state variables are not fixed before initialization and are released after initialization. This test ensures that the initialization process occurs as expected and that the state variables are appropriately managed throughout.\n", + "\n", + "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the aqueous property phase package. Similar tests can be written for the organic property package to ensure consistency and reliability across both packages." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "class TestStateBlock(object):\n", + " @pytest.fixture(scope=\"class\")\n", + " def model(self):\n", + " model = ConcreteModel()\n", + " model.params = AqPhase()\n", + "\n", + " model.props = model.params.build_state_block([1])\n", + "\n", + " return model\n", + "\n", + " @pytest.mark.unit\n", + " def test_build(self, model):\n", + " assert isinstance(model.props[1].flow_vol, Var)\n", + " assert value(model.props[1].flow_vol) == 1\n", + "\n", + " assert isinstance(model.props[1].temperature, Var)\n", + " assert value(model.props[1].temperature) == 300\n", + "\n", + " assert isinstance(model.props[1].conc_mass_comp, Var)\n", + " assert len(model.props[1].conc_mass_comp) == 3\n", + "\n", + " @pytest.mark.unit\n", + " def test_initialize(self, model):\n", + " assert not model.props[1].flow_vol.fixed\n", + " assert not model.props[1].temperature.fixed\n", + " assert not model.props[1].pressure.fixed\n", + " for i in model.props[1].conc_mass_comp:\n", + " assert not model.props[1].conc_mass_comp[i].fixed\n", + "\n", + " model.props.initialize(hold_state=False, outlvl=1)\n", + "\n", + " assert not model.props[1].flow_vol.fixed\n", + " assert not model.props[1].temperature.fixed\n", + " assert not model.props[1].pressure.fixed\n", + " for i in model.props[1].conc_mass_comp:\n", + " assert not model.props[1].conc_mass_comp[i].fixed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Component Tests\n", + "In the component test, we aim to ensure unit consistency across the entire property package. Unlike unit tests that focus on individual functions, component tests assess the coherence and consistency of the entire package. Here's what the component test will entail:\n", + "\n", + "Unit Consistency Check: Verify that all units used within the property package are consistent throughout. This involves checking that all parameters, variables, and equations within the package adhere to the same unit system, ensuring compatibility.\n", + "\n", + "By conducting a comprehensive component test, we can ensure that the property package functions as a cohesive unit, maintaining consistency and reliability across its entirety. This concludes our tests on the property package. Next we shall test the unit model. " + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [], + "source": [ + "@pytest.mark.component\n", + "def check_units(model):\n", + " model = ConcreteModel()\n", + " model.params = AqPhase()\n", + " assert_units_consistent(model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5.2 Unit Model\n", + "### Unit tests\n", + "Unit tests for the unit model encompass verifying the configuration arguments and the build function, similar to the approach taken for the property package. When testing the config arguments, we ensure that the correct number of arguments is provided and then match each argument with the expected one. This ensures that the unit model is properly configured and ready to operate as intended." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "import pytest\n", + "\n", + "from idaes.core import FlowsheetBlock\n", + "import idaes.models.unit_models\n", + "from idaes.core.solvers import get_solver\n", + "import idaes.logger as idaeslog\n", + "\n", + "\n", + "from pyomo.environ import value, check_optimal_termination, units\n", + "from pyomo.util.check_units import assert_units_consistent\n", + "from idaes.core.util.model_statistics import (\n", + " number_variables,\n", + " number_total_constraints,\n", + ")\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.initialization import (\n", + " SingleControlVolumeUnitInitializer,\n", + ")\n", + "\n", + "solver = get_solver()\n", + "\n", + "\n", + "@pytest.mark.unit\n", + "def test_config():\n", + " m = ConcreteModel()\n", + " m.fs = FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.unit = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + "\n", + " # Check unit config arguments\n", + " assert len(m.fs.unit.config) == 9\n", + "\n", + " # Check for config arguments\n", + " assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault\n", + " assert not m.fs.unit.config.has_pressure_change\n", + " assert not m.fs.unit.config.has_phase_equilibrium\n", + " assert m.fs.unit.config.organic_property_package is m.fs.org_properties\n", + " assert m.fs.unit.config.aqueous_property_package is m.fs.aq_properties\n", + "\n", + " # Check for unit initializer\n", + " assert m.fs.unit.default_initializer is SingleControlVolumeUnitInitializer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In testing the build function, we verify whether the number of variables aligns with the intended values and also check for the existence of desired constraints within the unit model. This ensures that the unit model is constructed accurately and includes all the necessary variables and constraints required for its proper functioning." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [], + "source": [ + "class TestBuild(object):\n", + " @pytest.fixture(scope=\"class\")\n", + " def model(self):\n", + " m = ConcreteModel()\n", + " m.fs = FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.unit = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + "\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.l / units.h)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.l)\n", + "\n", + " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.l / units.h)\n", + " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.l)\n", + "\n", + " return m\n", + "\n", + " @pytest.mark.build\n", + " @pytest.mark.unit\n", + " def test_build(self, model):\n", + "\n", + " assert hasattr(model.fs.unit, \"aqueous_inlet\")\n", + " assert len(model.fs.unit.aqueous_inlet.vars) == 4\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"organic_inlet\")\n", + " assert len(model.fs.unit.organic_inlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_inlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"aqueous_outlet\")\n", + " assert len(model.fs.unit.aqueous_outlet.vars) == 4\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"organic_outlet\")\n", + " assert len(model.fs.unit.organic_outlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_outlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"material_aq_balance\")\n", + " assert hasattr(model.fs.unit, \"material_org_balance\")\n", + "\n", + " assert number_variables(model) == 34\n", + " assert number_total_constraints(model) == 16" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Component tests\n", + "\n", + "During the component tests, we evaluate the performance of the unit model when integrated with the property package. This evaluation process typically involves several steps:\n", + "\n", + "1. Unit Consistency Check: Verify that the unit model maintains consistency in its units throughout the model. This ensures that all variables and constraints within the model adhere to the same unit system, guaranteeing compatibility.\n", + "\n", + "2. Termination Condition Verification: This involves checking whether the model terminates optimally with the given inlet conditions.\n", + "\n", + "3. Variable Value Assessment: Check the values of outlet variables against the expected values. To account for the numerical tolerance of the solvers, the values are compared using the approx function with a relative tolerance.\n", + "\n", + "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered.\n", + "\n", + "5. Structural Issues: Verify that there are no structural issues with the model. \n", + "\n", + "By performing these checks, we conclude the testing for the unit model. " + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "class TestFlowsheet:\n", + " @pytest.fixture\n", + " def model(self):\n", + " m = ConcreteModel()\n", + " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.unit = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + " )\n", + "\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", + "\n", + " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", + " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.kg)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.kg)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.kg)\n", + "\n", + " return m\n", + "\n", + " @pytest.mark.component\n", + " def test_unit_model(self, model):\n", + " assert_units_consistent(model)\n", + " solver = get_solver()\n", + " results = solver.solve(model, tee=False)\n", + "\n", + " # Check for optimal termination\n", + " assert check_optimal_termination(results)\n", + "\n", + " # Checking for outlet flows\n", + " assert value(model.fs.unit.organic_outlet.flow_vol[0]) == pytest.approx(\n", + " 80.0, rel=1e-5\n", + " )\n", + " assert value(model.fs.unit.aqueous_outlet.flow_vol[0]) == pytest.approx(\n", + " 10.0, rel=1e-5\n", + " )\n", + "\n", + " # Checking for outlet mass_comp\n", + " assert value(\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"CaSO4\"]\n", + " ) == pytest.approx(0.000187499, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"KNO3\"]\n", + " ) == pytest.approx(0.000749999, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"NaCl\"]\n", + " ) == pytest.approx(0.000403124, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"CaSO4\"]\n", + " ) == pytest.approx(0.0985, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"KNO3\"]\n", + " ) == pytest.approx(0.194, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"NaCl\"]\n", + " ) == pytest.approx(0.146775, rel=1e-5)\n", + "\n", + " # Checking for outlet temperature\n", + " assert value(model.fs.unit.organic_outlet.temperature[0]) == pytest.approx(\n", + " 300, rel=1e-5\n", + " )\n", + " assert value(model.fs.unit.aqueous_outlet.temperature[0]) == pytest.approx(\n", + " 300, rel=1e-5\n", + " )\n", + "\n", + " # Checking for outlet pressure\n", + " assert value(model.fs.unit.organic_outlet.pressure[0]) == pytest.approx(\n", + " 1, rel=1e-5\n", + " )\n", + " assert value(model.fs.unit.aqueous_outlet.pressure[0]) == pytest.approx(\n", + " 1, rel=1e-5\n", + " )\n", + "\n", + " # Fixed state variables\n", + " assert model.fs.unit.organic_inlet.flow_vol[0].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", + " assert model.fs.unit.organic_inlet.temperature[0].fixed\n", + " assert model.fs.unit.organic_inlet.pressure[0].fixed\n", + "\n", + " assert model.fs.unit.aqueous_inlet.flow_vol[0].fixed\n", + " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", + " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", + " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", + " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", + " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", + "\n", + " @pytest.mark.component\n", + " def test_structural_issues(self, model):\n", + " dt = DiagnosticsToolbox(model)\n", + " dt.assert_no_structural_warnings()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this tutorial, we have covered the comprehensive process of creating a custom unit model from scratch. Let's recap the key steps we have undertaken:\n", + "\n", + "- Developing property package\n", + "- Constructing the unit model \n", + "- Creating a Flowsheet\n", + "- Debugging the model using DiagnosticsToolbox\n", + "- Writing tests for the unit model\n", + "\n", + "By following the aforementioned procedure, one can create their own custom unit model. This concludes the tutorial on creating a custom unit model. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "examples-310-new", + "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.10.19" + } + }, + "nbformat": 4, + "nbformat_minor": 3 +} \ No newline at end of file diff --git a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_usr.ipynb b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_usr.ipynb index 211052dd..a057ec03 100644 --- a/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_usr.ipynb +++ b/idaes_examples/notebooks/docs/unit_models/custom_unit_models/creating_unit_model_usr.ipynb @@ -1,2245 +1,2408 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "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": [ - "# Creating Custom Unit Model\n", - "Author: Javal Vyas \n", - "Maintainer: Javal Vyas \n", - "Updated: 2023-02-20\n", - "\n", - "This tutorial is a comprehensive step-wise procedure to build a custom unit model from scratch. This tutorial will include creating a property package, a custom unit model and testing them. For this tutorial we shall create a custom unit model for Liquid - Liquid Extraction. \n", - "\n", - "The Liquid - Liquid Extractor model contains two immiscible fluids forming the two phases. One of the phases, say phase_1 has a high concentration of solutes which is to be separated. A mass transfer happens between the two phases and the solute is transferred from phase_1 to phase_2. This mass transfer is governed by a parameter called the distribution coefficient.\n", - "\n", - "After reviewing the working principles of the Liquid - Liquid Extractor, we shall proceed to create a custom unit model. We will require a property package for each phase, a custom unit model class and tests for the model and property packages.\n", - "\n", - "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", - "- Steady-state only\n", - "- Organic phase property package has a single phase named Org\n", - "- Aqueous phase property package has a single phase named Aq\n", - "- Organic and Aqueous phase properties need not have the same component list. \n", - "\n", - "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1. Creating Organic Property Package\n", - "\n", - "Creating a property package is a 4 step process\n", - "- Import necessary libraries \n", - "- Creating Physical Parameter Data Block\n", - "- Define State Block\n", - "- Define State Block Data\n", - "\n", - "# 1.1 Importing necessary packages \n", - "Let us begin with the importing the necessary libraries where we will be using functionalities from IDAES and Pyomo. " - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "# Import Python libraries\n", - "import logging\n", - "\n", - "import idaes.logger as idaeslog\n", - "from idaes.core.util.initialization import fix_state_vars\n", - "\n", - "# Import Pyomo libraries\n", - "from pyomo.environ import (\n", - " Param,\n", - " Set,\n", - " Var,\n", - " NonNegativeReals,\n", - " units,\n", - " Expression,\n", - " PositiveReals,\n", - ")\n", - "\n", - "# Import IDAES cores\n", - "from idaes.core import (\n", - " declare_process_block_class,\n", - " MaterialFlowBasis,\n", - " PhysicalParameterBlock,\n", - " StateBlockData,\n", - " StateBlock,\n", - " MaterialBalanceType,\n", - " EnergyBalanceType,\n", - " Solute,\n", - " Solvent,\n", - " LiquidPhase,\n", - ")\n", - "from idaes.core.util.model_statistics import degrees_of_freedom" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1.2 Physical Parameter Data Block\n", - "\n", - "A `PhysicalParameterBlock` serves as the central point of reference for all aspects of the property package and needs to define several things about the package. These are summarized below:\n", - "\n", - "- Units of measurement\n", - "- What properties are supported and how they are implemented\n", - "- What components and phases are included in the packages\n", - "- All the global parameters necessary for calculating properties\n", - "- A reference to the associated State Block class, so that construction of the State Block components can be automated from the Physical Parameter Block\n", - "\n", - "To construct this block, we begin by declaring a process block class using a Python decorator. One can learn more about `declare_process_block_class` [here](https://github.com/IDAES/idaes-pse/blob/eea1209077b75f7d940d8958362e69d4650c079d/idaes/core/base/process_block.py#L173). After constructing the process block, we define a build function which contains all the components that the property package would have. `super` function here is used to give access to methods and properties of a parent or sibling class and since this is used on the class `PhysicalParameterData` class, build has access to all the parent and sibling class methods. \n", - "\n", - "The `PhysicalParameterBlock` then refers to the `state block`, in this case `OrgPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we move on to list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. Like here since the solvent is in the Organic phase, we will assign the Phase as OrganicPhase and the variable will be named Org as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", - " \n", - "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the Organic phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. Additionally, a parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", - "\n", - "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). In this method, we will call the predefined method `add_default_units()`.\n", - "\n", - "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default. " - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "@declare_process_block_class(\"OrgPhase\")\n", - "class PhysicalParameterData(PhysicalParameterBlock):\n", - " \"\"\"\n", - " Property Parameter Block Class\n", - "\n", - " Contains parameters and indexing sets associated with properties for\n", - " organic Phase\n", - "\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction.\n", - " \"\"\"\n", - " super().build()\n", - "\n", - " self._state_block_class = OrgPhaseStateBlock\n", - "\n", - " # List of valid phases in property package\n", - " self.Org = LiquidPhase()\n", - "\n", - " # Component list - a list of component identifiers\n", - " self.NaCl = Solute()\n", - " self.KNO3 = Solute()\n", - " self.CaSO4 = Solute()\n", - " self.solvent = (\n", - " Solvent()\n", - " ) # Solvent used here is ethylene dibromide (Organic Polar)\n", - "\n", - " # Heat capacity of solvent\n", - " self.cp_mass = Param(\n", - " mutable=True,\n", - " initialize=717.01,\n", - " doc=\"Specific heat capacity of solvent\",\n", - " units=units.J / units.kg / units.K,\n", - " )\n", - "\n", - " self.dens_mass = Param(\n", - " mutable=True,\n", - " initialize=2170,\n", - " doc=\"Density of ethylene dibromide\",\n", - " units=units.kg / units.m**3,\n", - " )\n", - " self.temperature_ref = Param(\n", - " within=PositiveReals,\n", - " mutable=True,\n", - " default=298.15,\n", - " doc=\"Reference temperature\",\n", - " units=units.K,\n", - " )\n", - " self.diffusion_factor = Param(\n", - " self.solute_set,\n", - " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", - " within=PositiveReals,\n", - " mutable=True,\n", - " )\n", - "\n", - " @classmethod\n", - " def define_metadata(cls, obj):\n", - " obj.add_default_units(\n", - " {\n", - " \"time\": units.hour,\n", - " \"length\": units.m,\n", - " \"mass\": units.g,\n", - " \"amount\": units.mol,\n", - " \"temperature\": units.K,\n", - " }\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 1.3 State Block\n", - "\n", - "After the `PhysicalParameterBlock` class has been created, the next step is to write the code necessary to create the State Blocks that will be used throughout the flowsheet. `StateBlock` contains all the information necessary to define the state of the system. This includes the state variables and constraints on those variables which are used to describe a state property like the enthalpy, material balance, etc.\n", - "\n", - "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed StateBlock all at once (rather than element by element).\n", - "\n", - "The class `_OrganicStateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the `fix_initialization_states` function. `fix_initialization_states` function is to used to fix the state variable within the state block with the provided initial values (usually inlet conditions). It takes a `block` as the argument in which the state variables are to be fixed. It also takes `state_args` as an optional argument. `state_args` is a dictionary with the value for the state variables to be fixed. This function returns a dictionary indexed by the block, state variables and variable index indicating the fixed status of each variable before applying the function. \n", - "\n", - "The above function comprise of the _OrganicStateBlock, next we shall see the construction of the OrgPhaseStateBlockData class." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "class _OrganicStateBlock(StateBlock):\n", - " \"\"\"\n", - " This Class contains methods which should be applied to Property Blocks as a\n", - " whole, rather than individual elements of indexed Property Blocks.\n", - " \"\"\"\n", - "\n", - " def fix_initialization_states(self):\n", - " fix_state_vars(self)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The class `OrgPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `OrgPhaseStateBlock`, and inherits the block class from `_OrganicStateBlock`. This inheritance allows `OrgPhaseStateBlockData` to leverage functions from `_OrganicStateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", - "\n", - "The subsequent objective is to delineate the state variables, accomplished through the `_make_state_vars` method. This method encompasses all the essential state variables and associated data. For this particular property package, the required state variables are:\n", - "\n", - "- `flow_vol` - volumetric flow rate\n", - "- `conc_mass_comp` - mass fractions\n", - "- `pressure` - state pressure\n", - "- `temperature` - state temperature\n", - "\n", - "After establishing the state variables, the subsequent step involves setting up state properties as constraints. This includes specifying the relationships and limitations that dictate the system's behavior. The following properties need to be articulated:\n", - "\n", - "-`get_material_flow_terms`: quantifies the amount of material flow.\n", - "- `get_enthalpy_flow_terms`: quantifies the amount of enthalpy flow.\n", - "- `get_flow_rate`: details volumetric flow rates.\n", - "- `default_material_balance_type`: defines the kind of material balance to be used.\n", - "- `default_energy_balance_type`: defines the kind of energy balance to be used.\n", - "- `define_state_vars`: involves defining state variables with units, akin to the define_metadata function in the PhysicalParameterData block.\n", - "- `get_material_flow_basis`: establishes the basis on which state variables are measured, whether in mass or molar terms.\n", - "\n", - "These definitions mark the conclusion of the state block construction and thus the property package. For additional details on creating a property package, please refer to this [resource](../../properties/custom/custom_physical_property_packages_usr.ipynb ).\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_OrganicStateBlock)\n", - "class OrgPhaseStateBlockData(StateBlockData):\n", - " \"\"\"\n", - " An example property package for Organic phzase for liquid liquid extraction\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction\n", - " \"\"\"\n", - " super().build()\n", - " self._make_state_vars()\n", - "\n", - " def _make_state_vars(self):\n", - " self.flow_vol = Var(\n", - " initialize=1,\n", - " domain=NonNegativeReals,\n", - " doc=\"Total volumetric flowrate\",\n", - " units=units.L / units.hour,\n", - " )\n", - " self.conc_mass_comp = Var(\n", - " self.params.solute_set,\n", - " domain=NonNegativeReals,\n", - " initialize=1,\n", - " doc=\"Component mass concentrations\",\n", - " units=units.g / units.L,\n", - " )\n", - " self.pressure = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=1,\n", - " bounds=(1, 5),\n", - " units=units.atm,\n", - " doc=\"State pressure [atm]\",\n", - " )\n", - "\n", - " self.temperature = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=300,\n", - " bounds=(273, 373),\n", - " units=units.K,\n", - " doc=\"State temperature [K]\",\n", - " )\n", - "\n", - " def material_flow_expression(self, j):\n", - " if j == \"solvent\":\n", - " return self.flow_vol * self.params.dens_mass\n", - " else:\n", - " return self.flow_vol * self.conc_mass_comp[j]\n", - "\n", - " self.material_flow_expression = Expression(\n", - " self.component_list,\n", - " rule=material_flow_expression,\n", - " doc=\"Material flow terms\",\n", - " )\n", - "\n", - " def enthalpy_flow_expression(self):\n", - " return (\n", - " self.flow_vol\n", - " * self.params.dens_mass\n", - " * self.params.cp_mass\n", - " * (self.temperature - self.params.temperature_ref)\n", - " )\n", - "\n", - " self.enthalpy_flow_expression = Expression(\n", - " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", - " )\n", - "\n", - " def get_flow_rate(self):\n", - " return self.flow_vol\n", - "\n", - " def get_material_flow_terms(self, p, j):\n", - " return self.material_flow_expression[j]\n", - "\n", - " def get_enthalpy_flow_terms(self, p):\n", - " return self.enthalpy_flow_expression\n", - "\n", - " def default_material_balance_type(self):\n", - " return MaterialBalanceType.componentTotal\n", - "\n", - " def default_energy_balance_type(self):\n", - " return EnergyBalanceType.enthalpyTotal\n", - "\n", - " def define_state_vars(self):\n", - " return {\n", - " \"flow_vol\": self.flow_vol,\n", - " \"conc_mass_comp\": self.conc_mass_comp,\n", - " \"temperature\": self.temperature,\n", - " \"pressure\": self.pressure,\n", - " }\n", - "\n", - " def get_material_flow_basis(self):\n", - " return MaterialFlowBasis.mass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 2. Creating Aqueous Property Package\n", - "\n", - "The structure of Aqueous Property Package mirrors that of the Organic Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "# Import Python libraries\n", - "import logging\n", - "\n", - "from idaes.core.util.initialization import fix_state_vars\n", - "\n", - "# Import Pyomo libraries\n", - "from pyomo.environ import (\n", - " Param,\n", - " Var,\n", - " NonNegativeReals,\n", - " units,\n", - " Expression,\n", - " PositiveReals,\n", - ")\n", - "\n", - "# Import IDAES cores\n", - "from idaes.core import (\n", - " declare_process_block_class,\n", - " MaterialFlowBasis,\n", - " PhysicalParameterBlock,\n", - " StateBlockData,\n", - " StateBlock,\n", - " MaterialBalanceType,\n", - " EnergyBalanceType,\n", - " Solute,\n", - " Solvent,\n", - " LiquidPhase,\n", - ")\n", - "\n", - "# Some more information about this module\n", - "__author__ = \"Javal Vyas\"\n", - "\n", - "\n", - "# Set up logger\n", - "_log = logging.getLogger(__name__)\n", - "\n", - "\n", - "@declare_process_block_class(\"AqPhase\")\n", - "class AqPhaseData(PhysicalParameterBlock):\n", - " \"\"\"\n", - " Property Parameter Block Class\n", - "\n", - " Contains parameters and indexing sets associated with properties for\n", - " aqueous Phase\n", - "\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction.\n", - " \"\"\"\n", - " super().build()\n", - "\n", - " self._state_block_class = AqPhaseStateBlock\n", - "\n", - " # List of valid phases in property package\n", - " self.Aq = LiquidPhase()\n", - "\n", - " # Component list - a list of component identifiers\n", - " self.NaCl = Solute()\n", - " self.KNO3 = Solute()\n", - " self.CaSO4 = Solute()\n", - " self.H2O = Solvent()\n", - "\n", - " # Heat capacity of solvent\n", - " self.cp_mass = Param(\n", - " mutable=True,\n", - " initialize=4182,\n", - " doc=\"Specific heat capacity of solvent\",\n", - " units=units.J / units.kg / units.K,\n", - " )\n", - "\n", - " self.dens_mass = Param(\n", - " mutable=True,\n", - " initialize=997,\n", - " doc=\"Density of ethylene dibromide\",\n", - " units=units.kg / units.m**3,\n", - " )\n", - " self.temperature_ref = Param(\n", - " within=PositiveReals,\n", - " mutable=True,\n", - " default=298.15,\n", - " doc=\"Reference temperature\",\n", - " units=units.K,\n", - " )\n", - "\n", - " @classmethod\n", - " def define_metadata(cls, obj):\n", - " obj.add_default_units(\n", - " {\n", - " \"time\": units.hour,\n", - " \"length\": units.m,\n", - " \"mass\": units.g,\n", - " \"amount\": units.mol,\n", - " \"temperature\": units.K,\n", - " }\n", - " )\n", - "\n", - "\n", - "class _AqueousStateBlock(StateBlock):\n", - " \"\"\"\n", - " This Class contains methods which should be applied to Property Blocks as a\n", - " whole, rather than individual elements of indexed Property Blocks.\n", - " \"\"\"\n", - "\n", - " def fix_initialization_states(self):\n", - " fix_state_vars(self)\n", - "\n", - "\n", - "@declare_process_block_class(\"AqPhaseStateBlock\", block_class=_AqueousStateBlock)\n", - "class AqPhaseStateBlockData(StateBlockData):\n", - " \"\"\"\n", - " An example property package for ideal gas properties with Gibbs energy\n", - " \"\"\"\n", - "\n", - " def build(self):\n", - " \"\"\"\n", - " Callable method for Block construction\n", - " \"\"\"\n", - " super().build()\n", - " self._make_state_vars()\n", - "\n", - " def _make_state_vars(self):\n", - " self.flow_vol = Var(\n", - " initialize=1,\n", - " domain=NonNegativeReals,\n", - " doc=\"Total volumetric flowrate\",\n", - " units=units.L / units.hour,\n", - " )\n", - "\n", - " self.conc_mass_comp = Var(\n", - " self.params.solute_set,\n", - " domain=NonNegativeReals,\n", - " initialize={\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1},\n", - " doc=\"Component mass concentrations\",\n", - " units=units.g / units.L,\n", - " )\n", - "\n", - " self.pressure = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=1,\n", - " bounds=(1, 5),\n", - " units=units.atm,\n", - " doc=\"State pressure [atm]\",\n", - " )\n", - "\n", - " self.temperature = Var(\n", - " domain=NonNegativeReals,\n", - " initialize=300,\n", - " bounds=(273, 373),\n", - " units=units.K,\n", - " doc=\"State temperature [K]\",\n", - " )\n", - "\n", - " def material_flow_expression(self, j):\n", - " if j == \"H2O\":\n", - " return self.flow_vol * self.params.dens_mass\n", - " else:\n", - " return self.conc_mass_comp[j] * self.flow_vol\n", - "\n", - " self.material_flow_expression = Expression(\n", - " self.component_list,\n", - " rule=material_flow_expression,\n", - " doc=\"Material flow terms\",\n", - " )\n", - "\n", - " def enthalpy_flow_expression(self):\n", - " return (\n", - " self.flow_vol\n", - " * self.params.dens_mass\n", - " * self.params.cp_mass\n", - " * (self.temperature - self.params.temperature_ref)\n", - " )\n", - "\n", - " self.enthalpy_flow_expression = Expression(\n", - " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", - " )\n", - "\n", - " def get_flow_rate(self):\n", - " return self.flow_vol\n", - "\n", - " def get_material_flow_terms(self, p, j):\n", - " return self.material_flow_expression[j]\n", - "\n", - " def get_enthalpy_flow_terms(self, p):\n", - " return self.enthalpy_flow_expression\n", - "\n", - " def default_material_balance_type(self):\n", - " return MaterialBalanceType.componentTotal\n", - "\n", - " def default_energy_balance_type(self):\n", - " return EnergyBalanceType.enthalpyTotal\n", - "\n", - " def define_state_vars(self):\n", - " return {\n", - " \"flow_vol\": self.flow_vol,\n", - " \"conc_mass_comp\": self.conc_mass_comp,\n", - " \"temperature\": self.temperature,\n", - " \"pressure\": self.pressure,\n", - " }\n", - "\n", - " def get_material_flow_basis(self):\n", - " return MaterialFlowBasis.mass" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 3. Liquid Liquid Extractor Unit Model\n", - "\n", - "Following the creation of property packages, our next step is to develop a unit model that facilitates the mass transfer of solutes between phases. This involves importing necessary libraries, building the unit model, defining auxiliary functions, and establishing the initialization routine for the unit model.\n", - "\n", - "## 3.1 Importing necessary libraries\n", - "\n", - "Let's commence by importing the essential libraries from Pyomo and IDAES." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [], - "source": [ - "# Import Pyomo libraries\n", - "from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool\n", - "from pyomo.environ import (\n", - " value,\n", - " Constraint,\n", - " check_optimal_termination,\n", - ")\n", - "\n", - "# Import IDAES cores\n", - "from idaes.core import (\n", - " ControlVolume0DBlock,\n", - " declare_process_block_class,\n", - " MaterialBalanceType,\n", - " EnergyBalanceType,\n", - " MaterialFlowBasis,\n", - " MomentumBalanceType,\n", - " UnitModelBlockData,\n", - " useDefault,\n", - ")\n", - "from idaes.core.util.config import (\n", - " is_physical_parameter_block,\n", - " is_reaction_parameter_block,\n", - ")\n", - "\n", - "import idaes.logger as idaeslog\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.util.exceptions import ConfigurationError, InitializationError" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3.2 Creating the unit model\n", - "\n", - "Creating a unit model starts by creating a class called `LiqExtractionData` and use the `declare_process_block_class` decorator. The `LiqExtractionData` inherits the properties of `UnitModelBlockData` class, which allows us to create a control volume which is necessary for the unit model. After declaration of the class we proceed to define the relevant config arguments for the control volume. The config arguments includes the following properties:\n", - "\n", - "- `material_balance_type` - Indicates what type of mass balance should be constructed\n", - "- `has_pressure_change` - Indicates whether terms for pressure change should be\n", - "constructed\n", - "- `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", - "constructed\n", - "- `Organic Property` - Property parameter object used to define property calculations\n", - "for the Organic phase\n", - "- `Organic Property Arguments` - Arguments to use for constructing Organic phase properties\n", - "- `Aqueous Property` - Property parameter object used to define property calculations\n", - "for the aqueous phase\n", - "- `Aqueous Property Arguments` - Arguments to use for constructing aqueous phase properties\n", - "\n", - "As there are no pressure changes or reactions in this scenario, configuration arguments for these aspects are not included. However, additional details on configuration arguments can be found [here](https://github.com/IDAES/idaes-pse/blob/8948c6ce27d4c7f2c06b377a173f413599091998/idaes/models/unit_models/cstr.py)." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "@declare_process_block_class(\"LiqExtraction\")\n", - "class LiqExtractionData(UnitModelBlockData):\n", - " \"\"\"\n", - " LiqExtraction Unit Model Class\n", - " \"\"\"\n", - "\n", - " CONFIG = UnitModelBlockData.CONFIG()\n", - "\n", - " CONFIG.declare(\n", - " \"material_balance_type\",\n", - " ConfigValue(\n", - " default=MaterialBalanceType.useDefault,\n", - " domain=In(MaterialBalanceType),\n", - " description=\"Material balance construction flag\",\n", - " doc=\"\"\"Indicates what type of mass balance should be constructed,\n", - " **default** - MaterialBalanceType.useDefault.\n", - " **Valid values:** {\n", - " **MaterialBalanceType.useDefault - refer to property package for default\n", - " balance type\n", - " **MaterialBalanceType.none** - exclude material balances,\n", - " **MaterialBalanceType.componentPhase** - use phase component balances,\n", - " **MaterialBalanceType.componentTotal** - use total component balances,\n", - " **MaterialBalanceType.elementTotal** - use total element balances,\n", - " **MaterialBalanceType.total** - use total material balance.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_pressure_change\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Pressure change term construction flag\",\n", - " doc=\"\"\"Indicates whether terms for pressure change should be\n", - " constructed,\n", - " **default** - False.\n", - " **Valid values:** {\n", - " **True** - include pressure change terms,\n", - " **False** - exclude pressure change terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"has_phase_equilibrium\",\n", - " ConfigValue(\n", - " default=False,\n", - " domain=Bool,\n", - " description=\"Phase equilibrium construction flag\",\n", - " doc=\"\"\"Indicates whether terms for phase equilibrium should be\n", - " constructed,\n", - " **default** = False.\n", - " **Valid values:** {\n", - " **True** - include phase equilibrium terms\n", - " **False** - exclude phase equilibrium terms.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"organic_property_package\",\n", - " ConfigValue(\n", - " default=useDefault,\n", - " domain=is_physical_parameter_block,\n", - " description=\"Property package to use for organic phase\",\n", - " doc=\"\"\"Property parameter object used to define property calculations\n", - " for the organic phase,\n", - " **default** - useDefault.\n", - " **Valid values:** {\n", - " **useDefault** - use default package from parent model or flowsheet,\n", - " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"organic_property_package_args\",\n", - " ConfigBlock(\n", - " implicit=True,\n", - " description=\"Arguments to use for constructing organic phase properties\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to organic phase\n", - " property block(s) and used when constructing these,\n", - " **default** - None.\n", - " **Valid values:** {\n", - " see property package for documentation.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"aqueous_property_package\",\n", - " ConfigValue(\n", - " default=useDefault,\n", - " domain=is_physical_parameter_block,\n", - " description=\"Property package to use for aqueous phase\",\n", - " doc=\"\"\"Property parameter object used to define property calculations\n", - " for the aqueous phase,\n", - " **default** - useDefault.\n", - " **Valid values:** {\n", - " **useDefault** - use default package from parent model or flowsheet,\n", - " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", - " ),\n", - " )\n", - " CONFIG.declare(\n", - " \"aqueous_property_package_args\",\n", - " ConfigBlock(\n", - " implicit=True,\n", - " description=\"Arguments to use for constructing aqueous phase properties\",\n", - " doc=\"\"\"A ConfigBlock with arguments to be passed to aqueous phase\n", - " property block(s) and used when constructing these,\n", - " **default** - None.\n", - " **Valid values:** {\n", - " see property package for documentation.}\"\"\",\n", - " ),\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Building the model\n", - "\n", - "After constructing the `LiqExtractionData` block and defining the config arguments for the control block, the next step is to write a build function that incorporates control volume and establishes constraints on the control volume to achieve the desired mass transfer. The control volume serves as a pivotal component in the unit model construction, representing the volume in which the process unfolds.\n", - "\n", - "IDAES provides flexibility in choosing control volumes based on geometry, with options including 0D or 1D. In this instance, we opt for a 0D control volume, the most commonly used control volume. This choice is suitable for systems where there is a well-mixed volume of fluid or where spatial variations are deemed negligible.\n", - "\n", - "The control volume encompasses parameters from (1-8), and its equations are configured to satisfy the specified config arguments. For a more in-depth understanding, users are encouraged to refer to [this resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst). \n", - "\n", - "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Org' for the Organic phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", - "\n", - "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the Organic phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, and the property package, along with property package arguments. \n", - "\n", - "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the Organic property package\n", - "\n", - "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", - "\n", - "$\\frac{\\partial M_{t, p, j}}{\\partial t} = F_{in, t, p, j} - F_{out, t, p, j} + N_{kinetic, t, p, j} + N_{equilibrium, t, p, j} + N_{pe, t, p, j} + N_{transfer, t, p, j} + N_{custom, t, p, j}$\n", - "\n", - "- $\\frac{\\partial M_{t, p, j}}{\\partial t}$ - Material accumulation\n", - "- $F_{in, t, p, j}$ - Flow into the control volume\n", - "- $F_{out, t, p, j}$ - Flow out of the control volume\n", - "- $N_{kinetic, t, p, j}$ - Rate of reaction generation\n", - "- $N_{equilibrium, t, p, j}$ - Equilibrium reaction generation\n", - "- $N_{pe, t, p, j}$ - Equilibrium reaction extent\n", - "- $N_{transfer, t, p, j}$ - Mass transfer\n", - "- $N_{custom, t, p, j}$ - User defined terms in material balance\n", - "\n", - "- t indicates time index\n", - "- p indicates phase index\n", - "- j indicates component index\n", - "- e indicates element index\n", - "- r indicates reaction name index\n", - "\n", - "Here we shall see that $N_{transfer, t, p, j}$ is the term in the equation which is responsible for the mass transfer and the `mass_transfer_term` should only be equal to the amount being transferred and not include a material balance on our own. For a detailed description of the terms one should refer to the following [resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst)\n", - "\n", - "This concludes the creation of organic phase control volume. Similar procedure is done for the aqueous phase control volume with aqueous property package. \n", - "\n", - "Now, the unit model has two control volumes with appropriate configurations and material, momentum and energy balances. The next step is to check the basis of the two property packages. They should both have the same flow basis, and an error is raised if this is not the case.\n", - "\n", - "Following this, the `add_inlet_ports` and `add_outlet_ports` functions are used to create inlet and outlet ports. These ports are named and assigned to each control volume, resulting in labeled inlet and outlet ports for each control volume.\n", - "\n", - "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the organic phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{org} = - mass\\_transfer\\_term_{aq} $\n", - "\n", - "Here $mass\\_transfer\\_term_{p}$ is the term indicating the amount of material being transferred from/to the phase and $D_{i}$ is the Distribution co-efficient for component i. \n", - "\n", - "This marks the completion of the build function, and the unit model is now equipped with the necessary process constraints. The subsequent steps involve writing the initialization routine." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "def build(self):\n", - " \"\"\"\n", - " Begin building model (pre-DAE transformation).\n", - " Args:\n", - " None\n", - " Returns:\n", - " None\n", - " \"\"\"\n", - " # Call UnitModel.build to setup dynamics\n", - " super().build()\n", - "\n", - " # Check phase lists match assumptions\n", - " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the aquoues \"\n", - " f\"phase property package have a single phase named 'Aq'\"\n", - " )\n", - " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", - " f\"phase property package have a single phase named 'Org'\"\n", - " )\n", - "\n", - " # Check for at least one common component in component lists\n", - " if not any(\n", - " j in self.config.aqueous_property_package.component_list\n", - " for j in self.config.organic_property_package.component_list\n", - " ):\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", - " f\"and aqueous phase property packages have at least one \"\n", - " f\"common component.\"\n", - " )\n", - "\n", - " self.organic_phase = ControlVolume0DBlock(\n", - " dynamic=self.config.dynamic,\n", - " property_package=self.config.organic_property_package,\n", - " property_package_args=self.config.organic_property_package_args,\n", - " )\n", - "\n", - " self.organic_phase.add_state_blocks(\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium\n", - " )\n", - "\n", - " # Separate organic and aqueous phases means that phase equilibrium will\n", - " # be handled at the unit model level, thus has_phase_equilibrium is\n", - " # False, but has_mass_transfer is True.\n", - "\n", - " self.organic_phase.add_material_balances(\n", - " balance_type=self.config.material_balance_type,\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", - " has_mass_transfer=True,\n", - " )\n", - " # ---------------------------------------------------------------------\n", - "\n", - " self.aqueous_phase = ControlVolume0DBlock(\n", - " dynamic=self.config.dynamic,\n", - " property_package=self.config.aqueous_property_package,\n", - " property_package_args=self.config.aqueous_property_package_args,\n", - " )\n", - "\n", - " self.aqueous_phase.add_state_blocks(\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium\n", - " )\n", - "\n", - " # Separate liquid and aqueous phases means that phase equilibrium will\n", - " # be handled at the unit model level, thus has_phase_equilibrium is\n", - " # False, but has_mass_transfer is True.\n", - "\n", - " self.aqueous_phase.add_material_balances(\n", - " balance_type=self.config.material_balance_type,\n", - " # has_rate_reactions=False,\n", - " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", - " has_mass_transfer=True,\n", - " )\n", - "\n", - " self.aqueous_phase.add_geometry()\n", - "\n", - " # ---------------------------------------------------------------------\n", - " # Check flow basis is compatible\n", - " t_init = self.flowsheet().time.first()\n", - " if (\n", - " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", - " ):\n", - " raise ConfigurationError(\n", - " f\"{self.name} aqueous and organic property packages must use the \"\n", - " f\"same material flow basis.\"\n", - " )\n", - "\n", - " self.organic_phase.add_geometry()\n", - "\n", - " # Add Ports\n", - " self.add_inlet_port(\n", - " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", - " )\n", - " self.add_inlet_port(\n", - " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", - " )\n", - " self.add_outlet_port(\n", - " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", - " )\n", - " self.add_outlet_port(\n", - " name=\"aqueous_outlet\",\n", - " block=self.aqueous_phase,\n", - " doc=\"Aqueous outlet\",\n", - " )\n", - "\n", - " # ---------------------------------------------------------------------\n", - " # Add unit level constraints\n", - " # First, need the union and intersection of component lists\n", - " all_comps = (\n", - " self.aqueous_phase.properties_out.component_list\n", - " | self.organic_phase.properties_out.component_list\n", - " )\n", - " common_comps = (\n", - " self.aqueous_phase.properties_out.component_list\n", - " & self.organic_phase.properties_out.component_list\n", - " )\n", - "\n", - " # Get units for unit conversion\n", - " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", - " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", - " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", - "\n", - " if flow_basis == MaterialFlowBasis.mass:\n", - " fb = \"flow_mass\"\n", - " elif flow_basis == MaterialFlowBasis.molar:\n", - " fb = \"flow_mole\"\n", - " else:\n", - " raise ConfigurationError(\n", - " f\"{self.name} Liquid-Liquid Extractor only supports mass \"\n", - " f\"basis for MaterialFlowBasis.\"\n", - " )\n", - "\n", - " # Material balances\n", - " def rule_material_aq_balance(self, t, j):\n", - " if j in common_comps:\n", - " return self.aqueous_phase.mass_transfer_term[\n", - " t, \"Aq\", j\n", - " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", - " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", - " )\n", - " elif j in self.organic_phase.properties_out.component_list:\n", - " # No mass transfer term\n", - " # Set organic flowrate to an arbitrary small value\n", - " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", - " elif j in self.aqueous_phase.properties_out.component_list:\n", - " # No mass transfer term\n", - " # Set aqueous flowrate to an arbitrary small value\n", - " return self.aqueous_phase.mass_transfer_term[t, \"Aq\", j] == 0 * aunits(fb)\n", - "\n", - " self.material_aq_balance = Constraint(\n", - " self.flowsheet().time,\n", - " self.aqueous_phase.properties_out.component_list,\n", - " rule=rule_material_aq_balance,\n", - " doc=\"Unit level material balances for Aq\",\n", - " )\n", - "\n", - " def rule_material_liq_balance(self, t, j):\n", - " if j in common_comps:\n", - " return (\n", - " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", - " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", - " )\n", - " else:\n", - " # No mass transfer term\n", - " # Set organic flowrate to an arbitrary small value\n", - " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", - "\n", - " self.material_org_balance = Constraint(\n", - " self.flowsheet().time,\n", - " self.organic_phase.properties_out.component_list,\n", - " rule=rule_material_liq_balance,\n", - " doc=\"Unit level material balances Org\",\n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Initialization Routine\n", - "\n", - "After writing the unit model it is crucial to initialize the model properly, as non-linear models may encounter local minima or infeasibility if not initialized properly. IDAES provides us with a few initialization routines which may not work for all the models, and in such cases the developer will have to define their own initialization routines. \n", - "\n", - "To create a custom initialization routine, model developers must create an initialize method as part of their model, and provide a sequence of steps intended to build up a feasible solution. Initialization routines generally make use of Pyomo’s tools for activating and deactivating constraints and often involve solving multiple sub-problems whilst building up an initial state.\n", - "\n", - "For this tutorial we would use the pre-defined initialization routine of `BlockTriangularizationInitializer` when initializing the model in the flowsheet. This Initializer should be suitable for most models, but may struggle to initialize\n", - "tightly coupled systems of equations. This method of initialization will follow the following workflow. \n", - "\n", - "- Have precheck for structural singularity\n", - "- Run incidence analysis on given block data and check matching.\n", - "- Call Block Triangularization solver on model.\n", - "- Call solve_strongly_connected_components on a given BlockData.\n", - "\n", - "For more details about this initialization routine can be found [here](https://github.com/IDAES/idaes-pse/blob/c09433b9afed5ae2fe25c0ccdc732783324f0101/idaes/core/initialization/block_triangularization.py). \n", - "\n", - "\n", - "This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_usr.ipynb). The next sections will deal with the diagonistics and testing of the property package and unit model. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 3.3 Building a Flowsheet\n", - "\n", - "Once we have set up the unit model and its property packages, we can start building a flowsheet using them. In this tutorial, we're focusing on a simple flowsheet with just a liquid-liquid extractor. To create the flowsheet we follow the following steps:\n", - "\n", - "- Import necessary libraries\n", - "- Create a Pyomo model.\n", - "- Inside the model, create a flowsheet block.\n", - "- Assign property packages to the flowsheet block.\n", - "- Add the liquid-liquid extractor to the flowsheet block.\n", - "- Fix variable to make it a square problem\n", - "- Run an initialization process.\n", - "- Solve the flowsheet.\n", - "\n", - "Following these steps, we've built a basic flowsheet using Pyomo. For more details, refer to the [documentation](../../flowsheets/hda_flowsheet_with_distillation_usr.ipynb).\n" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "import pyomo.environ as pyo\n", - "import idaes.core\n", - "import idaes.models.unit_models\n", - "from idaes.core.solvers import get_solver\n", - "import idaes.logger as idaeslog\n", - "from pyomo.network import Arc\n", - "from idaes.core.util.model_statistics import degrees_of_freedom\n", - "from idaes.core.initialization import InitializationStatus\n", - "from idaes.core.initialization.block_triangularization import (\n", - " BlockTriangularizationInitializer,\n", - ")\n", - "from liquid_extraction.organic_property import OrgPhase\n", - "from liquid_extraction.aqueous_property import AqPhase\n", - "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", - "\n", - "\n", - "def build_model():\n", - " m = pyo.ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.lex = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - " return m\n", - "\n", - "\n", - "def fix_state_variables(m):\n", - " m.fs.lex.organic_inlet.flow_vol.fix(80 * pyo.units.L / pyo.units.hour)\n", - " m.fs.lex.organic_inlet.temperature.fix(300 * pyo.units.K)\n", - " m.fs.lex.organic_inlet.pressure.fix(1 * pyo.units.atm)\n", - " m.fs.lex.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", - " 1e-5 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", - " 1e-5 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", - " 1e-5 * pyo.units.g / pyo.units.L\n", - " )\n", - "\n", - " m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", - " m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", - " m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", - " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", - " 0.15 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", - " 0.2 * pyo.units.g / pyo.units.L\n", - " )\n", - " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", - " 0.1 * pyo.units.g / pyo.units.L\n", - " )\n", - "\n", - " return m\n", - "\n", - "\n", - "def initialize_model(m):\n", - " initializer = BlockTriangularizationInitializer()\n", - " initializer.initialize(m.fs.lex)\n", - " return m\n", - "\n", - "\n", - "def main():\n", - " m = build_model()\n", - " m = fix_state_variables(m)\n", - " m = initialize_model(m)\n", - " return m\n", - "\n", - "\n", - "if __name__ == main:\n", - " main()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 4. Model Diagnostics using DiagnosticsToolbox\n", - "\n", - "Here, during initialization, we encounter warnings indicating that variables are being set to negative values, which is not expected behavior. These warnings suggest that there may be flaws in the model that require further investigation using the DiagnosticsToolbox from IDAES. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter_usr.ipynb).\n", - "\n", - "To proceed with investigating these issues, we need to import the DiagnosticsToolbox. We can gain a better understanding of its functionality by running the help function on it. " - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "from idaes.core.util import DiagnosticsToolbox" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The help() function provides comprehensive information on the DiagnosticsToolbox and all its supported methods. However, it's essential to focus on the initial steps outlined at the beginning of the docstring to get started effectively.\n", - "\n", - "Here's a breakdown of the steps to start with:\n", - "\n", - "- `Instantiate Model:` Ensure you have an instance of the model with a degrees of freedom equal to 0.\n", - "\n", - "- `Create DiagnosticsToolbox Instance:` Next, instantiate a DiagnosticsToolbox object.\n", - "\n", - "- `Provide Model to DiagnosticsToolbox:` Pass the model instance to the DiagnosticsToolbox.\n", - "\n", - "- `Call report_structural_issues() Function:` Finally, call the report_structural_issues() function. This function will highlight any warnings in the model's structure, such as unit inconsistencies or other issues related to variables in the caution section.\n", - "\n", - "By following these steps, you can efficiently utilize the DiagnosticsToolbox to identify and address any structural issues or warnings in your model." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ + "cells": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "WARNING (W1001): Setting Var\n", - "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", - "`-0.1725` (float) not in domain NonNegativeReals.\n", - " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", - "WARNING (W1001): Setting Var\n", - "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", - "`-0.4` (float) not in domain NonNegativeReals.\n", - " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", - "WARNING (W1001): Setting Var\n", - "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", - "`-0.05` (float) not in domain NonNegativeReals.\n", - " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", - "====================================================================================\n", - "Model Statistics\n", - "\n", - " Activated Blocks: 21 (Deactivated: 0)\n", - " Free Variables in Activated Constraints: 16 (External: 0)\n", - " Free Variables with only lower bounds: 8\n", - " Free Variables with only upper bounds: 0\n", - " Free Variables with upper and lower bounds: 0\n", - " Fixed Variables in Activated Constraints: 8 (External: 0)\n", - " Activated Equality Constraints: 16 (Deactivated: 0)\n", - " Activated Inequality Constraints: 0 (Deactivated: 0)\n", - " Activated Objectives: 0 (Deactivated: 0)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "0 WARNINGS\n", - "\n", - " No warnings found!\n", - "\n", - "------------------------------------------------------------------------------------\n", - "1 Cautions\n", - "\n", - " Caution: 10 unused variables (4 fixed)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "Suggested next steps:\n", - "\n", - " Try to initialize/solve your model and then call report_numerical_issues()\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "m = main()\n", - "dt = DiagnosticsToolbox(m)\n", - "dt.report_structural_issues()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Although no warnings were reported, it's important to note that there are 3 variables fixed to 0 and 10 unused variables, out of which 4 are fixed. As indicated in the output, the next step is to solve the model. After solving, you should call the report_numerical_issues() function. This function will help identify any numerical issues that may arise during the solution process." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "###############################################################################\n", + "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n", + "# Framework (IDAES IP) was produced under the DOE Institute for the\n", + "# Design of Advanced Energy Systems (IDAES).\n", + "#\n", + "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n", + "# University of California, through Lawrence Berkeley National Laboratory,\n", + "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n", + "# University, West Virginia University Research Corporation, et al.\n", + "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n", + "# for full copyright and license information.\n", + "###############################################################################" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "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...: 33\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 14\n", - "\n", - "Total number of variables............................: 16\n", - " variables with only lower bounds: 8\n", - " variables with lower and upper bounds: 0\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 16\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 4.00e+01 4.93e+01 -1.0 4.10e-01 - 9.91e-01 2.41e-02h 1\n", - " 2 0.0000000e+00 4.00e+01 2.03e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", - " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", - " 4r 0.0000000e+00 4.00e+01 9.88e+04 1.6 3.68e+02 - 9.92e-01 2.29e-03f 1\n", - " 5r 0.0000000e+00 3.60e+01 3.03e+00 1.6 4.01e+00 - 1.00e+00 1.00e+00f 1\n", - " 6r 0.0000000e+00 3.69e+01 1.21e+01 -1.2 9.24e-01 - 9.69e-01 9.78e-01f 1\n", - " 7r 0.0000000e+00 3.70e+01 2.11e-01 -1.9 1.00e-01 - 9.97e-01 1.00e+00f 1\n", - " 8r 0.0000000e+00 3.78e+01 2.03e-02 -4.3 8.71e-01 - 9.71e-01 1.00e+00f 1\n", - " 9r 0.0000000e+00 3.80e+01 2.62e-04 -6.4 1.24e-01 - 9.99e-01 1.00e+00f 1\n", - "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", - " 10r 0.0000000e+00 3.81e+01 5.87e-09 -6.4 1.58e-01 - 1.00e+00 1.00e+00f 1\n", - " 11r 0.0000000e+00 3.91e+01 1.09e-05 -9.0 9.35e-01 - 9.68e-01 1.00e+00f 1\n", - "\n", - "Number of Iterations....: 11\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 5.1393961893966849e-07 5.1393961893966849e-07\n", - "Constraint violation....: 3.9105165554489545e+01 3.9105165554489545e+01\n", - "Complementarity.........: 9.0909090910996620e-10 9.0909090910996620e-10\n", - "Overall NLP error.......: 3.9105165554489545e+01 3.9105165554489545e+01\n", - "\n", - "\n", - "Number of objective function evaluations = 17\n", - "Number of objective gradient evaluations = 5\n", - "Number of equality constraint evaluations = 17\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 14\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 12\n", - "Total CPU secs in IPOPT (w/o function evaluations) = 0.004\n", - "Total CPU secs in NLP function evaluations = 0.000\n", - "\n", - "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", - "WARNING: Loading a SolverResults object with a warning status into\n", - "model.name=\"unknown\";\n", - " - termination condition: infeasible\n", - " - message from solver: Ipopt 3.13.2\\x3a Converged to a locally infeasible\n", - " point. Problem may be infeasible.\n" - ] + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Creating Custom Unit Model\n", + "Author: Javal Vyas \n", + "Maintainer: Javal Vyas \n", + "\n", + "This tutorial is a comprehensive step-wise procedure to build a custom unit model from scratch. This tutorial will include creating a property package, a custom unit model and testing them. For this tutorial we shall create a custom unit model for Liquid - Liquid Extraction. \n", + "\n", + "The Liquid - Liquid Extractor model contains two immiscible fluids forming the two phases. One of the phases, say phase_1 has a high concentration of solutes which is to be separated. A mass transfer happens between the two phases and the solute is transferred from phase_1 to phase_2. This mass transfer is governed by a parameter called the distribution coefficient.\n", + "\n", + "After reviewing the working principles of the Liquid - Liquid Extractor, we shall proceed to create a custom unit model. We will require a property package for each phase, a custom unit model class and tests for the model and property packages.\n", + "\n", + "Before commencing the development of the model, we need to state some assumptions which the following unit model will be using. \n", + "- Steady-state only\n", + "- Organic phase property package has a single phase named Org\n", + "- Aqueous phase property package has a single phase named Aq\n", + "- Organic and Aqueous phase properties need not have the same component list. \n", + "\n", + "Thus as per the assumptions, we will be creating one property package for the aqueous phase (Aq), and the other for the Organic phase (Org). " + ] }, { - "data": { - "text/plain": [ - "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06552338600158691}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1. Creating Organic Property Package\n", + "\n", + "Creating a property package is a 4 step process\n", + "- Import necessary libraries \n", + "- Creating Physical Parameter Data Block\n", + "- Define State Block\n", + "- Define State Block Data\n", + "\n", + "# 1.1 Importing necessary packages \n", + "Let us begin with importing the necessary libraries where we will be using functionalities from IDAES and Pyomo. " ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "solver = pyo.SolverFactory(\"ipopt\")\n", - "solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The model is probably infeasible thus indicating numerical issues with the model. We should call the `report_numerical_issues()` function and check what the constraints/variables causing this issue. " - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "====================================================================================\n", - "Model Statistics\n", - "\n", - " Jacobian Condition Number: 7.955E+03\n", - "\n", - "------------------------------------------------------------------------------------\n", - "2 WARNINGS\n", - "\n", - " WARNING: 6 Constraints with large residuals (>1.0E-05)\n", - " WARNING: 5 Variables at or outside bounds (tol=0.0E+00)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "3 Cautions\n", - "\n", - " Caution: 8 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", - " Caution: 5 Variables with value close to zero (tol=1.0E-08)\n", - " Caution: 3 Variables with extreme value (<1.0E-04 or >1.0E+04)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "Suggested next steps:\n", - "\n", - " display_constraints_with_large_residuals()\n", - " display_variables_at_or_outside_bounds()\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "dt.report_numerical_issues()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this scenario, it's observed that the condition number of the Jacobian is high, indicating that the Jacobian is ill-conditioned. Additionally, there are 2 warnings related to constraints with large residuals and variables at or outside the bounds. The cautions mentioned in the output are also related to these warnings.\n", - "\n", - "As suggested, the next steps would be to:\n", - "\n", - "- Call the `display_variables_at_or_outside_bounds()` function to investigate variables at or outside the bounds.\n", - "\n", - "- Call the `display_constraints_with_large_residuals()` function to examine constraints with large residuals.\n", - "\n", - "These steps will help identify the underlying causes of the numerical issues and constraints violations, allowing for further analysis and potential resolution. " - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Python libraries\n", + "import logging\n", + "\n", + "import idaes.logger as idaeslog\n", + "from idaes.core.util.initialization import fix_state_vars\n", + "\n", + "# Import Pyomo libraries\n", + "from pyomo.environ import (\n", + " Param,\n", + " Set,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", + "\n", + "# Import IDAES cores\n", + "from idaes.core import (\n", + " declare_process_block_class,\n", + " MaterialFlowBasis,\n", + " PhysicalParameterBlock,\n", + " StateBlockData,\n", + " StateBlock,\n", + " MaterialBalanceType,\n", + " EnergyBalanceType,\n", + " Solute,\n", + " Solvent,\n", + " LiquidPhase,\n", + ")\n", + "from idaes.core.util.model_statistics import degrees_of_freedom" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "====================================================================================\n", - "The following variable(s) have values at or outside their bounds (tol=0.0E+00):\n", - "\n", - " fs.lex.organic_phase.properties_in[0.0].pressure (fixed): value=1.0 bounds=(1, 5)\n", - " fs.lex.organic_phase.properties_out[0.0].pressure (free): value=1 bounds=(1, 5)\n", - " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl] (free): value=0.0 bounds=(0, None)\n", - " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3] (free): value=0.0 bounds=(0, None)\n", - " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4] (free): value=0.0 bounds=(0, None)\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "dt.display_variables_at_or_outside_bounds()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this scenario, there are a couple of issues to address:\n", - "\n", - "- The pressure variable is fixed to 1, which is its lower bound. This could potentially lead to numerical issues, although it may not affect the model significantly since there is no pressure change in the model. To mitigate this, consider adjusting the lower bound of the pressure variable to avoid having its value at or outside the bounds.\n", - "\n", - "- The more concerning issue is with the `conc_mass_comp` variable attempting to go below 0 in the output. This suggests that there may be constraints involving `conc_mass_comp` in the aqueous phase causing this behavior. To investigate further, it's recommended to call the `display_constraints_with_large_residuals()` function. This will provide insights into whether constraints involving `conc_mass_comp` are contributing to the convergence issue." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1.2 Physical Parameter Data Block\n", + "\n", + "A `PhysicalParameterBlock` serves as the central point of reference for all aspects of the property package and needs to define several things about the package. These are summarized below:\n", + "\n", + "- Units of measurement\n", + "- What properties are supported and how they are implemented\n", + "- What components and phases are included in the packages\n", + "- All the global parameters necessary for calculating properties\n", + "- A reference to the associated State Block class, so that construction of the State Block components can be automated from the Physical Parameter Block\n", + "\n", + "To construct this block, we begin by declaring a process block class using a Python decorator. One can learn more about `declare_process_block_class` [here](https://github.com/IDAES/idaes-pse/blob/eea1209077b75f7d940d8958362e69d4650c079d/idaes/core/base/process_block.py#L173). After constructing the process block, we define a build function which contains all the components that the property package would have. `super` function here is used to give access to methods and properties of a parent or sibling class and since this is used on the class `PhysicalParameterData` class, build has access to all the parent and sibling class methods. \n", + "\n", + "The `PhysicalParameterBlock` then refers to the `state block`, in this case `OrgPhaseStateBlock` (which will be declared later), so that we can build a state block instance by only knowing the `PhysicalParameterBlock` we wish to use. Then we list the number of phases in this property package. Then we assign the variable to the phase which follows a naming convention. The solvent is in the Organic phase; we will assign the Phase as OrganicPhase, and the variable will be named Org as per the naming convention. The details of naming conventions can be found [here](https://github.com/IDAES/idaes-pse/blob/main/docs/explanations/conventions.rst). We will be following the same convention throughout the example. \n", + " \n", + "After defining the list of the phases, we move on to list the components and their type in the phase. It can be a solute or a solvent in the Organic phase. Thus, we define the component and assign it to either being a solute or a solvent. In this case, the salts are the solutes and Ethylene dibromide is the solvent. Next, we define the physical properties involved in the package, like the heat capacity and density of the solvent, the reference temperature, and the distribution factor that would govern the mass transfer from one phase into another. Additionally, a parameter, the `diffusion_factor`, is introduced. This factor plays a crucial role in governing mass transfer between phases, necessitating its definition within the state block.\n", + "\n", + "The final step in creating the Physical Parameter Block is to declare a `classmethod` named `define_metadata`, which takes two arguments: a class (cls) and an instance of that class (obj). In this method, we will call the predefined method `add_default_units()`.\n", + "\n", + "- `obj.add_default_units()` sets the default units metadata for the property package, and here we define units to be used with this property package as default. " + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "====================================================================================\n", - "The following constraint(s) have large residuals (>1.0E-05):\n", - "\n", - " fs.lex.material_aq_balance[0.0,NaCl]: 5.49716E-01\n", - " fs.lex.material_aq_balance[0.0,KNO3]: 8.94833E-01\n", - " fs.lex.material_aq_balance[0.0,CaSO4]: 5.48843E-02\n", - " fs.lex.aqueous_phase.material_balances[0.0,NaCl]: 1.67003E+01\n", - " fs.lex.aqueous_phase.material_balances[0.0,KNO3]: 3.91052E+01\n", - " fs.lex.aqueous_phase.material_balances[0.0,CaSO4]: 4.94512E+00\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "dt.display_constraints_with_large_residuals()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As expected there are convergence issues with the constraints which have `conc_mass_comp` variable in them specifically in the aqeous phase. Now, let us investigate further by printing this constraints and checking the value of each term. Since this is an persistent issue across the components, we can focus on just one of the component to identify the issue. " - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "@declare_process_block_class(\"OrgPhase\")\n", + "class PhysicalParameterData(PhysicalParameterBlock):\n", + " \"\"\"\n", + " Property Parameter Block Class\n", + "\n", + " Contains parameters and indexing sets associated with properties for\n", + " organic Phase\n", + "\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction.\n", + " \"\"\"\n", + " super().build()\n", + "\n", + " self._state_block_class = OrgPhaseStateBlock\n", + "\n", + " # List of valid phases in property package\n", + " self.Org = LiquidPhase()\n", + "\n", + " # Component list - a list of component identifiers\n", + " self.NaCl = Solute()\n", + " self.KNO3 = Solute()\n", + " self.CaSO4 = Solute()\n", + " self.solvent = (\n", + " Solvent()\n", + " ) # Solvent used here is ethylene dibromide (Organic Polar)\n", + "\n", + " # Heat capacity of solvent\n", + " self.cp_mass = Param(\n", + " mutable=True,\n", + " initialize=717.01,\n", + " doc=\"Specific heat capacity of solvent\",\n", + " units=units.J / units.kg / units.K,\n", + " )\n", + "\n", + " self.dens_mass = Param(\n", + " mutable=True,\n", + " initialize=2170,\n", + " doc=\"Density of ethylene dibromide\",\n", + " units=units.kg / units.m**3,\n", + " )\n", + " self.temperature_ref = Param(\n", + " within=PositiveReals,\n", + " mutable=True,\n", + " default=298.15,\n", + " doc=\"Reference temperature\",\n", + " units=units.K,\n", + " )\n", + " self.diffusion_factor = Param(\n", + " self.solute_set,\n", + " initialize={\"NaCl\": 2.15, \"KNO3\": 3, \"CaSO4\": 1.5},\n", + " within=PositiveReals,\n", + " mutable=True,\n", + " )\n", + "\n", + " @classmethod\n", + " def define_metadata(cls, obj):\n", + " obj.add_default_units(\n", + " {\n", + " \"time\": units.hour,\n", + " \"length\": units.m,\n", + " \"mass\": units.g,\n", + " \"amount\": units.mol,\n", + " \"temperature\": units.K,\n", + " }\n", + " )" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{Member of material_balances} : Material balances\n", - " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", - " Key : Lower : Body : Upper : Active\n", - " (0.0, 'NaCl') : 0.0 : (fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) - (fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_out[0.0].flow_vol) + fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] : 0.0 : True\n" - ] - } - ], - "source": [ - "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 1.3 State Block\n", + "\n", + "After the `PhysicalParameterBlock` class has been created, the next step is to write the code necessary to create the State Blocks that will be used throughout the flowsheet. `StateBlock` contains all the information necessary to define the state of the system. This includes the state variables and constraints on those variables which are used to describe a state property like the enthalpy, material balance, etc.\n", + "\n", + "Creating a State Block requires us to write two classes. The reason we write two classes is because of the inherent nature of how `declare_process_block_data` works. `declare_process_block_data` facilitates creating an `IndexedComponent` object which can handle multiple `ComponentData` objects which represent the component at each point in the indexing set. This makes it easier to build an instance of the model at each indexed point. However, State Blocks are slightly different, as they are always indexed (at least by time). Due to this, we often want to perform actions on all the elements of the indexed StateBlock all at once (rather than element by element).\n", + "\n", + "The class `_OrganicStateBlock` is defined without the `declare_process_block_data` decorator and thus works as a traditional class and this facilitates performing a method on the class as a whole rather than individual elements of the indexed property blocks. In this class we define the `fix_initialization_states` function. `fix_initialization_states` function is used to fix the state variable within the state block with the provided initial values (usually inlet conditions). It takes a `block` as the argument in which the state variables are to be fixed. It also takes `state_args` as an optional argument. `state_args` is a dictionary with the value for the state variables to be fixed. This function returns a dictionary indexed by the block, state variables and variable index indicating the fixed status of each variable before applying the function. \n", + "\n", + "The above function comprise of the _OrganicStateBlock. Next, we shall see the construction of the OrgPhaseStateBlockData class." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{Member of conc_mass_comp} : Component mass concentrations\n", - " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " NaCl : 0 : 0.15 : None : True : True : NonNegativeReals\n", - "flow_vol : Total volumetric flowrate\n", - " Size=1, Index=None, Units=l/h\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " None : 0 : 100.0 : None : True : True : NonNegativeReals\n", - "{Member of conc_mass_comp} : Component mass concentrations\n", - " Size=3, Index=fs.aq_properties.solutes, Units=g/l\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " NaCl : 0 : 0.0 : None : False : False : NonNegativeReals\n", - "flow_vol : Total volumetric flowrate\n", - " Size=1, Index=None, Units=l/h\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " None : 0 : 100.0 : None : False : False : NonNegativeReals\n", - "{Member of mass_transfer_term} : Component material transfer into unit\n", - " Size=4, Index=fs._time*fs.aq_properties._phase_component_set, Units=g/h\n", - " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", - " (0.0, 'Aq', 'NaCl') : None : -31.700284300098897 : None : False : False : Reals\n" - ] - } - ], - "source": [ - "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", - "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", - "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", - "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", - "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "It seems there is a discrepancy between the mass transfer term and the amount of input of NaCl. This can be inferred from the values where the input equals 15g/h and the `mass_transfer_term` equals -31.706g/h.\n", - "\n", - "To further investigate this issue, it's advisable to examine the `material_aq_balance` constraint within the unit model where the `mass_transfer_term` is defined. By printing out this constraint and analyzing its components, you can gain a better understanding of the discrepancy and take appropriate corrective actions." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "class _OrganicStateBlock(StateBlock):\n", + " \"\"\"\n", + " This Class contains methods which should be applied to Property Blocks as a\n", + " whole, rather than individual elements of indexed Property Blocks.\n", + " \"\"\"\n", + "\n", + " def fix_initialization_states(self):\n", + " fix_state_vars(self)" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "{Member of material_aq_balance} : Unit level material balances for Aq\n", - " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", - " Key : Lower : Body : Upper : Active\n", - " (0.0, 'NaCl') : 0.0 : fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] + fs.org_properties.diffusion_factor[NaCl]*(fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) : 0.0 : True\n" - ] - } - ], - "source": [ - "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here the problem can be tracked down easily as there being a typing error while recording the distribution factor. The distribution factor here was wrongly written ignoring its magnitude which should have been 1e-2, but that was missed, thus adjusting the distribution factor parameter we should have this issue resolved. " - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [], - "source": [ - "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", - ")\n", - "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", - ")\n", - "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", - ")\n", - "\n", - "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", - "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "After the corrective actions, we should check if this have made any structural issues, for this we would call `report_structural_issues()`" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The class `OrgPhaseStateBlockData` is designated with the `declare_process_block_class` decorator, named `OrgPhaseStateBlock`, and inherits the block class from `_OrganicStateBlock`. This inheritance allows `OrgPhaseStateBlockData` to leverage functions from `_OrganicStateBlock`. Following the class definition, a build function similar to the one used in the `PhysicalParameterData` block is employed. The super function is utilized to enable the utilization of functions from the parent or sibling class.\n", + "\n", + "The subsequent objective is to delineate the state variables, accomplished through the `_make_state_vars` method. This method encompasses all the essential state variables and associated data. For this particular property package, the required state variables are:\n", + "\n", + "- `flow_vol` - volumetric flow rate\n", + "- `conc_mass_comp` - mass fractions\n", + "- `pressure` - state pressure\n", + "- `temperature` - state temperature\n", + "\n", + "After establishing the state variables, the subsequent step involves setting up state properties as constraints. This includes specifying the relationships and limitations that dictate the system's behavior. The following properties need to be articulated:\n", + "\n", + "-`get_material_flow_terms`: quantifies the amount of material flow.\n", + "- `get_enthalpy_flow_terms`: quantifies the amount of enthalpy flow.\n", + "- `get_flow_rate`: details volumetric flow rates.\n", + "- `default_material_balance_type`: defines the kind of material balance to be used.\n", + "- `default_energy_balance_type`: defines the kind of energy balance to be used.\n", + "- `define_state_vars`: involves defining state variables with units, akin to the define_metadata function in the PhysicalParameterData block.\n", + "- `get_material_flow_basis`: establishes the basis on which state variables are measured, whether in mass or molar terms.\n", + "\n", + "These definitions mark the conclusion of the state block construction and thus the property package. For additional details on creating a property package, please refer to this [resource](../../properties/custom/custom_physical_property_packages_usr.ipynb ).\n", + "\n" + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "====================================================================================\n", - "Model Statistics\n", - "\n", - " Activated Blocks: 21 (Deactivated: 0)\n", - " Free Variables in Activated Constraints: 16 (External: 0)\n", - " Free Variables with only lower bounds: 8\n", - " Free Variables with only upper bounds: 0\n", - " Free Variables with upper and lower bounds: 0\n", - " Fixed Variables in Activated Constraints: 8 (External: 0)\n", - " Activated Equality Constraints: 16 (Deactivated: 0)\n", - " Activated Inequality Constraints: 0 (Deactivated: 0)\n", - " Activated Objectives: 0 (Deactivated: 0)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "0 WARNINGS\n", - "\n", - " No warnings found!\n", - "\n", - "------------------------------------------------------------------------------------\n", - "1 Cautions\n", - "\n", - " Caution: 10 unused variables (4 fixed)\n", - "\n", - "------------------------------------------------------------------------------------\n", - "Suggested next steps:\n", - "\n", - " Try to initialize/solve your model and then call report_numerical_issues()\n", - "\n", - "====================================================================================\n" - ] - } - ], - "source": [ - "dt.report_structural_issues()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now since there are no warnings we can go ahead and solve the model and see if the results are optimal. " - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "@declare_process_block_class(\"OrgPhaseStateBlock\", block_class=_OrganicStateBlock)\n", + "class OrgPhaseStateBlockData(StateBlockData):\n", + " \"\"\"\n", + " An example property package for Organic phase for liquid liquid extraction\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction\n", + " \"\"\"\n", + " super().build()\n", + " self._make_state_vars()\n", + "\n", + " def _make_state_vars(self):\n", + " self.flow_vol = Var(\n", + " initialize=1,\n", + " domain=NonNegativeReals,\n", + " doc=\"Total volumetric flowrate\",\n", + " units=units.L / units.hour,\n", + " )\n", + " self.conc_mass_comp = Var(\n", + " self.params.solute_set,\n", + " domain=NonNegativeReals,\n", + " initialize=1,\n", + " doc=\"Component mass concentrations\",\n", + " units=units.g / units.L,\n", + " )\n", + " self.pressure = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=1,\n", + " bounds=(1, 5),\n", + " units=units.atm,\n", + " doc=\"State pressure [atm]\",\n", + " )\n", + "\n", + " self.temperature = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=300,\n", + " bounds=(273, 373),\n", + " units=units.K,\n", + " doc=\"State temperature [K]\",\n", + " )\n", + "\n", + " def material_flow_expression(self, j):\n", + " if j == \"solvent\":\n", + " return self.flow_vol * self.params.dens_mass\n", + " else:\n", + " return self.flow_vol * self.conc_mass_comp[j]\n", + "\n", + " self.material_flow_expression = Expression(\n", + " self.component_list,\n", + " rule=material_flow_expression,\n", + " doc=\"Material flow terms\",\n", + " )\n", + "\n", + " def enthalpy_flow_expression(self):\n", + " return (\n", + " self.flow_vol\n", + " * self.params.dens_mass\n", + " * self.params.cp_mass\n", + " * (self.temperature - self.params.temperature_ref)\n", + " )\n", + "\n", + " self.enthalpy_flow_expression = Expression(\n", + " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", + " )\n", + "\n", + " def get_flow_rate(self):\n", + " return self.flow_vol\n", + "\n", + " def get_material_flow_terms(self, p, j):\n", + " return self.material_flow_expression[j]\n", + "\n", + " def get_enthalpy_flow_terms(self, p):\n", + " return self.enthalpy_flow_expression\n", + "\n", + " def default_material_balance_type(self):\n", + " return MaterialBalanceType.componentTotal\n", + "\n", + " def default_energy_balance_type(self):\n", + " return EnergyBalanceType.enthalpyTotal\n", + "\n", + " def define_state_vars(self):\n", + " return {\n", + " \"flow_vol\": self.flow_vol,\n", + " \"conc_mass_comp\": self.conc_mass_comp,\n", + " \"temperature\": self.temperature,\n", + " \"pressure\": self.pressure,\n", + " }\n", + "\n", + " def get_material_flow_basis(self):\n", + " return MaterialFlowBasis.mass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 2. Creating Aqueous Property Package\n", + "\n", + "The structure of the Aqueous Property Package mirrors that of the Organic Property Package we previously developed. We'll commence with an overview, importing the required libraries, followed by the creation of the physical property block and two state blocks. The distinctions in this package lie in the physical parameter values, and notably, the absence of the diffusion factor term, differentiating it from the prior package. The following code snippet should provide clarity on these distinctions." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Python libraries\n", + "import logging\n", + "\n", + "from idaes.core.util.initialization import fix_state_vars\n", + "\n", + "# Import Pyomo libraries\n", + "from pyomo.environ import (\n", + " Param,\n", + " Var,\n", + " NonNegativeReals,\n", + " units,\n", + " Expression,\n", + " PositiveReals,\n", + ")\n", + "\n", + "# Import IDAES cores\n", + "from idaes.core import (\n", + " declare_process_block_class,\n", + " MaterialFlowBasis,\n", + " PhysicalParameterBlock,\n", + " StateBlockData,\n", + " StateBlock,\n", + " MaterialBalanceType,\n", + " EnergyBalanceType,\n", + " Solute,\n", + " Solvent,\n", + " LiquidPhase,\n", + ")\n", + "\n", + "# Some more information about this module\n", + "__author__ = \"Javal Vyas\"\n", + "\n", + "\n", + "# Set up logger\n", + "_log = logging.getLogger(__name__)\n", + "\n", + "\n", + "@declare_process_block_class(\"AqPhase\")\n", + "class AqPhaseData(PhysicalParameterBlock):\n", + " \"\"\"\n", + " Property Parameter Block Class\n", + "\n", + " Contains parameters and indexing sets associated with properties for\n", + " aqueous Phase\n", + "\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction.\n", + " \"\"\"\n", + " super().build()\n", + "\n", + " self._state_block_class = AqPhaseStateBlock\n", + "\n", + " # List of valid phases in property package\n", + " self.Aq = LiquidPhase()\n", + "\n", + " # Component list - a list of component identifiers\n", + " self.NaCl = Solute()\n", + " self.KNO3 = Solute()\n", + " self.CaSO4 = Solute()\n", + " self.H2O = Solvent()\n", + "\n", + " # Heat capacity of solvent\n", + " self.cp_mass = Param(\n", + " mutable=True,\n", + " initialize=4182,\n", + " doc=\"Specific heat capacity of solvent\",\n", + " units=units.J / units.kg / units.K,\n", + " )\n", + "\n", + " self.dens_mass = Param(\n", + " mutable=True,\n", + " initialize=997,\n", + " doc=\"Density of ethylene dibromide\",\n", + " units=units.kg / units.m**3,\n", + " )\n", + " self.temperature_ref = Param(\n", + " within=PositiveReals,\n", + " mutable=True,\n", + " default=298.15,\n", + " doc=\"Reference temperature\",\n", + " units=units.K,\n", + " )\n", + "\n", + " @classmethod\n", + " def define_metadata(cls, obj):\n", + " obj.add_default_units(\n", + " {\n", + " \"time\": units.hour,\n", + " \"length\": units.m,\n", + " \"mass\": units.g,\n", + " \"amount\": units.mol,\n", + " \"temperature\": units.K,\n", + " }\n", + " )\n", + "\n", + "\n", + "class _AqueousStateBlock(StateBlock):\n", + " \"\"\"\n", + " This Class contains methods which should be applied to Property Blocks as a\n", + " whole, rather than individual elements of indexed Property Blocks.\n", + " \"\"\"\n", + "\n", + " def fix_initialization_states(self):\n", + " fix_state_vars(self)\n", + "\n", + "\n", + "@declare_process_block_class(\"AqPhaseStateBlock\", block_class=_AqueousStateBlock)\n", + "class AqPhaseStateBlockData(StateBlockData):\n", + " \"\"\"\n", + " An example property package for ideal gas properties with Gibbs energy\n", + " \"\"\"\n", + "\n", + " def build(self):\n", + " \"\"\"\n", + " Callable method for Block construction\n", + " \"\"\"\n", + " super().build()\n", + " self._make_state_vars()\n", + "\n", + " def _make_state_vars(self):\n", + " self.flow_vol = Var(\n", + " initialize=1,\n", + " domain=NonNegativeReals,\n", + " doc=\"Total volumetric flowrate\",\n", + " units=units.L / units.hour,\n", + " )\n", + "\n", + " self.conc_mass_comp = Var(\n", + " self.params.solute_set,\n", + " domain=NonNegativeReals,\n", + " initialize={\"NaCl\": 0.15, \"KNO3\": 0.2, \"CaSO4\": 0.1},\n", + " doc=\"Component mass concentrations\",\n", + " units=units.g / units.L,\n", + " )\n", + "\n", + " self.pressure = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=1,\n", + " bounds=(1, 5),\n", + " units=units.atm,\n", + " doc=\"State pressure [atm]\",\n", + " )\n", + "\n", + " self.temperature = Var(\n", + " domain=NonNegativeReals,\n", + " initialize=300,\n", + " bounds=(273, 373),\n", + " units=units.K,\n", + " doc=\"State temperature [K]\",\n", + " )\n", + "\n", + " def material_flow_expression(self, j):\n", + " if j == \"H2O\":\n", + " return self.flow_vol * self.params.dens_mass\n", + " else:\n", + " return self.conc_mass_comp[j] * self.flow_vol\n", + "\n", + " self.material_flow_expression = Expression(\n", + " self.component_list,\n", + " rule=material_flow_expression,\n", + " doc=\"Material flow terms\",\n", + " )\n", + "\n", + " def enthalpy_flow_expression(self):\n", + " return (\n", + " self.flow_vol\n", + " * self.params.dens_mass\n", + " * self.params.cp_mass\n", + " * (self.temperature - self.params.temperature_ref)\n", + " )\n", + "\n", + " self.enthalpy_flow_expression = Expression(\n", + " rule=enthalpy_flow_expression, doc=\"Enthalpy flow term\"\n", + " )\n", + "\n", + " def get_flow_rate(self):\n", + " return self.flow_vol\n", + "\n", + " def get_material_flow_terms(self, p, j):\n", + " return self.material_flow_expression[j]\n", + "\n", + " def get_enthalpy_flow_terms(self, p):\n", + " return self.enthalpy_flow_expression\n", + "\n", + " def default_material_balance_type(self):\n", + " return MaterialBalanceType.componentTotal\n", + "\n", + " def default_energy_balance_type(self):\n", + " return EnergyBalanceType.enthalpyTotal\n", + "\n", + " def define_state_vars(self):\n", + " return {\n", + " \"flow_vol\": self.flow_vol,\n", + " \"conc_mass_comp\": self.conc_mass_comp,\n", + " \"temperature\": self.temperature,\n", + " \"pressure\": self.pressure,\n", + " }\n", + "\n", + " def get_material_flow_basis(self):\n", + " return MaterialFlowBasis.mass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 3. Liquid Liquid Extractor Unit Model\n", + "\n", + "Following the creation of property packages, our next step is to develop a unit model that facilitates the mass transfer of solutes between phases. This involves importing necessary libraries, building the unit model, defining auxiliary functions, and establishing the initialization routine for the unit model.\n", + "\n", + "## 3.1 Importing necessary libraries\n", + "\n", + "Let's commence by importing the essential libraries from Pyomo and IDAES." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "# Import Pyomo libraries\n", + "from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool\n", + "from pyomo.environ import (\n", + " value,\n", + " Constraint,\n", + " check_optimal_termination,\n", + ")\n", + "\n", + "# Import IDAES cores\n", + "from idaes.core import (\n", + " ControlVolume0DBlock,\n", + " declare_process_block_class,\n", + " MaterialBalanceType,\n", + " EnergyBalanceType,\n", + " MaterialFlowBasis,\n", + " MomentumBalanceType,\n", + " UnitModelBlockData,\n", + " useDefault,\n", + ")\n", + "from idaes.core.util.config import (\n", + " is_physical_parameter_block,\n", + " is_reaction_parameter_block,\n", + ")\n", + "\n", + "import idaes.logger as idaeslog\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.util.model_statistics import degrees_of_freedom\n", + "from idaes.core.util.exceptions import ConfigurationError, InitializationError" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3.2 Creating the unit model\n", + "\n", + "Creating a unit model starts by creating a class called `LiqExtractionData` and using the `declare_process_block_class` decorator. The `LiqExtractionData` inherits the properties of the `UnitModelBlockData` class, which allows us to create a control volume that is necessary for the unit model. After declaration of the class we proceed to define the relevant config arguments for the control volume. The config arguments include the following properties:\n", + "\n", + "- `material_balance_type` - Indicates what type of mass balance should be constructed\n", + "- `has_pressure_change` - Indicates whether terms for pressure change should be\n", + "constructed\n", + "- `has_phase_equilibrium` - Indicates whether terms for phase equilibrium should be\n", + "constructed\n", + "- `organic_property_package` - Property parameter object used to define property calculations\n", + "for the Organic phase\n", + "- `organic_property_package_args` - Arguments to use for constructing Organic phase properties\n", + "- `aqueous_property_package` - Property parameter object used to define property calculations\n", + "for the aqueous phase\n", + "- `aqueous_property_package_args` - Arguments to use for constructing aqueous phase properties\n", + "\n", + "As there are no pressure changes or reactions in this scenario, configuration arguments for these aspects are not included. However, additional details on configuration arguments can be found [here](https://github.com/IDAES/idaes-pse/blob/8948c6ce27d4c7f2c06b377a173f413599091998/idaes/models/unit_models/cstr.py)." + ] + }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "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...: 33\n", - "Number of nonzeros in inequality constraint Jacobian.: 0\n", - "Number of nonzeros in Lagrangian Hessian.............: 14\n", - "\n", - "Total number of variables............................: 16\n", - " variables with only lower bounds: 8\n", - " variables with lower and upper bounds: 0\n", - " variables with only upper bounds: 0\n", - "Total number of equality constraints.................: 16\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 5.85e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", - " 1 0.0000000e+00 3.55e-15 8.41e+00 -1.0 5.85e+01 - 1.05e-01 1.00e+00h 1\n", - "\n", - "Number of Iterations....: 1\n", - "\n", - " (scaled) (unscaled)\n", - "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Constraint violation....: 3.5527136788005009e-15 3.5527136788005009e-15\n", - "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", - "Overall NLP error.......: 3.5527136788005009e-15 3.5527136788005009e-15\n", - "\n", - "\n", - "Number of objective function evaluations = 2\n", - "Number of objective gradient evaluations = 2\n", - "Number of equality constraint evaluations = 2\n", - "Number of inequality constraint evaluations = 0\n", - "Number of equality constraint Jacobian evaluations = 2\n", - "Number of inequality constraint Jacobian evaluations = 0\n", - "Number of Lagrangian Hessian evaluations = 1\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" - ] + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "@declare_process_block_class(\"LiqExtraction\")\n", + "class LiqExtractionData(UnitModelBlockData):\n", + " \"\"\"\n", + " LiqExtraction Unit Model Class\n", + " \"\"\"\n", + "\n", + " CONFIG = UnitModelBlockData.CONFIG()\n", + "\n", + " CONFIG.declare(\n", + " \"material_balance_type\",\n", + " ConfigValue(\n", + " default=MaterialBalanceType.useDefault,\n", + " domain=In(MaterialBalanceType),\n", + " description=\"Material balance construction flag\",\n", + " doc=\"\"\"Indicates what type of mass balance should be constructed,\n", + " **default** - MaterialBalanceType.useDefault.\n", + " **Valid values:** {\n", + " **MaterialBalanceType.useDefault - refer to property package for default\n", + " balance type\n", + " **MaterialBalanceType.none** - exclude material balances,\n", + " **MaterialBalanceType.componentPhase** - use phase component balances,\n", + " **MaterialBalanceType.componentTotal** - use total component balances,\n", + " **MaterialBalanceType.elementTotal** - use total element balances,\n", + " **MaterialBalanceType.total** - use total material balance.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"has_pressure_change\",\n", + " ConfigValue(\n", + " default=False,\n", + " domain=Bool,\n", + " description=\"Pressure change term construction flag\",\n", + " doc=\"\"\"Indicates whether terms for pressure change should be\n", + " constructed,\n", + " **default** - False.\n", + " **Valid values:** {\n", + " **True** - include pressure change terms,\n", + " **False** - exclude pressure change terms.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"has_phase_equilibrium\",\n", + " ConfigValue(\n", + " default=False,\n", + " domain=Bool,\n", + " description=\"Phase equilibrium construction flag\",\n", + " doc=\"\"\"Indicates whether terms for phase equilibrium should be\n", + " constructed,\n", + " **default** = False.\n", + " **Valid values:** {\n", + " **True** - include phase equilibrium terms\n", + " **False** - exclude phase equilibrium terms.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"organic_property_package\",\n", + " ConfigValue(\n", + " default=useDefault,\n", + " domain=is_physical_parameter_block,\n", + " description=\"Property package to use for organic phase\",\n", + " doc=\"\"\"Property parameter object used to define property calculations\n", + " for the organic phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"organic_property_package_args\",\n", + " ConfigBlock(\n", + " implicit=True,\n", + " description=\"Arguments to use for constructing organic phase properties\",\n", + " doc=\"\"\"A ConfigBlock with arguments to be passed to organic phase\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"aqueous_property_package\",\n", + " ConfigValue(\n", + " default=useDefault,\n", + " domain=is_physical_parameter_block,\n", + " description=\"Property package to use for aqueous phase\",\n", + " doc=\"\"\"Property parameter object used to define property calculations\n", + " for the aqueous phase,\n", + " **default** - useDefault.\n", + " **Valid values:** {\n", + " **useDefault** - use default package from parent model or flowsheet,\n", + " **PropertyParameterObject** - a PropertyParameterBlock object.}\"\"\",\n", + " ),\n", + " )\n", + " CONFIG.declare(\n", + " \"aqueous_property_package_args\",\n", + " ConfigBlock(\n", + " implicit=True,\n", + " description=\"Arguments to use for constructing aqueous phase properties\",\n", + " doc=\"\"\"A ConfigBlock with arguments to be passed to aqueous phase\n", + " property block(s) and used when constructing these,\n", + " **default** - None.\n", + " **Valid values:** {\n", + " see property package for documentation.}\"\"\",\n", + " ),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Building the model\n", + "\n", + "After constructing the `LiqExtractionData` block and defining the config arguments for the control block, the next step is to write a build function that incorporates the control volume and establishes constraints on the control volume to achieve the desired mass transfer. The control volume serves as a pivotal component in the unit model construction, representing the volume in which the process unfolds.\n", + "\n", + "IDAES provides flexibility in choosing control volumes based on geometry, with options including 0D or 1D. In this instance, we opt for a 0D control volume, the most commonly used control volume. This choice is suitable for systems where there is a well-mixed volume of fluid or where spatial variations are deemed negligible.\n", + "\n", + "The control volume encompasses parameters from (1-8), and its equations are configured to satisfy the specified config arguments. For a more in-depth understanding, users are encouraged to refer to [this resource](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst). \n", + "\n", + "The `build` function is initiated using the `super` function to gain access to methods and properties of a parent or sibling class, in this case, the `LiqExtractionData` class. Following the `super` function, checks are performed on the property packages to ensure the appropriate names for the solvents, such as 'Aq' for the aqueous phase and 'Org' for the organic phase. An error is raised if these conditions are not met. Subsequently, a check is performed to ensure there is at least one common component between the two property packages that can be transferred from one phase to another.\n", + "\n", + "After these checks are completed without any exceptions raised, it is ensured that the property packages have the desired components with appropriate names. The next step is to create a control volume and assign it to a property package. Here, we initiate with the organic phase and attach a 0D control volume to it. The control volume takes arguments about the dynamics of the block, and the property package, along with property package arguments. \n", + "\n", + "The subsequent steps involve adding inlet and outlet state blocks to the control volume using the `add_state_blocks` function. This function takes arguments about the flow direction (defaulted to forward) and a flag for `has_phase_equilibrium`, which is read from the config. The control volume is now equipped with the inlet and outlet state blocks and has access to the organic property package\n", + "\n", + "Next, material balance equations are added to the control volume using the `add_material_balance` function, taking into account the type of material balance, `has_phase_equilibrium`, and the presence of `has_mass_transfer`. To understand this arguments further let us have a look at the material balance equation and how it is implemented in control volume. \n", + "\n", + "$\\frac{\\partial M_{t, p, j}}{\\partial t} = F_{in, t, p, j} - F_{out, t, p, j} + N_{kinetic, t, p, j} + N_{equilibrium, t, p, j} + N_{pe, t, p, j} + N_{transfer, t, p, j} + N_{custom, t, p, j}$\n", + "\n", + "- $\\frac{\\partial M_{t, p, j}}{\\partial t}$ - Material accumulation\n", + "- $F_{in, t, p, j}$ - Flow into the control volume\n", + "- $F_{out, t, p, j}$ - Flow out of the control volume\n", + "- $N_{kinetic, t, p, j}$ - Rate of reaction generation\n", + "- $N_{equilibrium, t, p, j}$ - Equilibrium reaction generation\n", + "- $N_{pe, t, p, j}$ - Equilibrium reaction extent\n", + "- $N_{transfer, t, p, j}$ - Mass transfer\n", + "- $N_{custom, t, p, j}$ - User defined terms in material balance\n", + "\n", + "- t indicates time index\n", + "- p indicates phase index\n", + "- j indicates component index\n", + "- e indicates element index\n", + "- r indicates reaction name index\n", + "\n", + "Here we shall see that $N_{transfer, t, p, j}$ is the term in the equation which is responsible for the mass transfer and the `mass_transfer_term` should only be equal to the amount being transferred and not include a material balance on our own. For a detailed description of the terms one should refer to the following [resource.](https://github.com/IDAES/idaes-pse/blob/2f34dd3abc1bce5ba17c80939a01f9034e4fbeef/docs/reference_guides/core/control_volume_0d.rst)\n", + "\n", + "This concludes the creation of the organic phase control volume. A similar procedure is done for the aqueous phase control volume with aqueous property package. \n", + "\n", + "Now, the unit model has two control volumes with appropriate configurations and material, momentum and energy balances. The next step is to check the basis of the two property packages. They should both have the same flow basis, and an error is raised if this is not the case.\n", + "\n", + "Following this, the `add_inlet_ports` and `add_outlet_ports` functions are used to create inlet and outlet ports. These ports are named and assigned to each control volume, resulting in labeled inlet and outlet ports for each control volume.\n", + "\n", + "The subsequent steps involve writing unit-level constraints. A check if the basis is either molar or mass, and unit-level constraints are written accordingly. The first constraint pertains to the mass transfer term for the aqueous phase. The mass transfer term is equal to $mass\\_transfer\\_term_{aq} = (D_{i})\\frac{mass_{i}~in~aq~phase}{flowrate~of~aq~phase}$. The second constraint relates to the mass transfer term in the organic phase, which is the negative of the mass transfer term in the aqueous phase: $mass\\_transfer\\_term_{org} = - mass\\_transfer\\_term_{aq} $\n", + "\n", + "Here $mass\\_transfer\\_term_{p}$ is the term indicating the amount of material being transferred from/to the phase and $D_{i}$ is the Distribution coefficient for component i. \n", + "\n", + "This marks the completion of the build function, and the unit model is now equipped with the necessary process constraints. The subsequent steps involve writing the initialization routine." + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "def build(self):\n", + " \"\"\"\n", + " Begin building model (pre-DAE transformation).\n", + " Args:\n", + " None\n", + " Returns:\n", + " None\n", + " \"\"\"\n", + " # Call UnitModel.build to setup dynamics\n", + " super().build()\n", + "\n", + " # Check phase lists match assumptions\n", + " if self.config.aqueous_property_package.phase_list != [\"Aq\"]:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the aqueous \"\n", + " f\"phase property package have a single phase named 'Aq'\"\n", + " )\n", + " if self.config.organic_property_package.phase_list != [\"Org\"]:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"phase property package have a single phase named 'Org'\"\n", + " )\n", + "\n", + " # Check for at least one common component in component lists\n", + " if not any(\n", + " j in self.config.aqueous_property_package.component_list\n", + " for j in self.config.organic_property_package.component_list\n", + " ):\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor model requires that the organic \"\n", + " f\"and aqueous phase property packages have at least one \"\n", + " f\"common component.\"\n", + " )\n", + "\n", + " self.organic_phase = ControlVolume0DBlock(\n", + " dynamic=self.config.dynamic,\n", + " property_package=self.config.organic_property_package,\n", + " property_package_args=self.config.organic_property_package_args,\n", + " )\n", + "\n", + " self.organic_phase.add_state_blocks(\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium\n", + " )\n", + "\n", + " # Separate organic and aqueous phases means that phase equilibrium will\n", + " # be handled at the unit model level, thus has_phase_equilibrium is\n", + " # False, but has_mass_transfer is True.\n", + "\n", + " self.organic_phase.add_material_balances(\n", + " balance_type=self.config.material_balance_type,\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", + " has_mass_transfer=True,\n", + " )\n", + " # ---------------------------------------------------------------------\n", + "\n", + " self.aqueous_phase = ControlVolume0DBlock(\n", + " dynamic=self.config.dynamic,\n", + " property_package=self.config.aqueous_property_package,\n", + " property_package_args=self.config.aqueous_property_package_args,\n", + " )\n", + "\n", + " self.aqueous_phase.add_state_blocks(\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium\n", + " )\n", + "\n", + " # Separate liquid and aqueous phases means that phase equilibrium will\n", + " # be handled at the unit model level, thus has_phase_equilibrium is\n", + " # False, but has_mass_transfer is True.\n", + "\n", + " self.aqueous_phase.add_material_balances(\n", + " balance_type=self.config.material_balance_type,\n", + " # has_rate_reactions=False,\n", + " has_phase_equilibrium=self.config.has_phase_equilibrium,\n", + " has_mass_transfer=True,\n", + " )\n", + "\n", + " self.aqueous_phase.add_geometry()\n", + "\n", + " # ---------------------------------------------------------------------\n", + " # Check flow basis is compatible\n", + " t_init = self.flowsheet().time.first()\n", + " if (\n", + " self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", + " != self.organic_phase.properties_out[t_init].get_material_flow_basis()\n", + " ):\n", + " raise ConfigurationError(\n", + " f\"{self.name} aqueous and organic property packages must use the \"\n", + " f\"same material flow basis.\"\n", + " )\n", + "\n", + " self.organic_phase.add_geometry()\n", + "\n", + " # Add Ports\n", + " self.add_inlet_port(\n", + " name=\"organic_inlet\", block=self.organic_phase, doc=\"Organic feed\"\n", + " )\n", + " self.add_inlet_port(\n", + " name=\"aqueous_inlet\", block=self.aqueous_phase, doc=\"Aqueous feed\"\n", + " )\n", + " self.add_outlet_port(\n", + " name=\"organic_outlet\", block=self.organic_phase, doc=\"Organic outlet\"\n", + " )\n", + " self.add_outlet_port(\n", + " name=\"aqueous_outlet\",\n", + " block=self.aqueous_phase,\n", + " doc=\"Aqueous outlet\",\n", + " )\n", + "\n", + " # ---------------------------------------------------------------------\n", + " # Add unit level constraints\n", + " # First, need the union and intersection of component lists\n", + " all_comps = (\n", + " self.aqueous_phase.properties_out.component_list\n", + " | self.organic_phase.properties_out.component_list\n", + " )\n", + " common_comps = (\n", + " self.aqueous_phase.properties_out.component_list\n", + " & self.organic_phase.properties_out.component_list\n", + " )\n", + "\n", + " # Get units for unit conversion\n", + " aunits = self.config.aqueous_property_package.get_metadata().get_derived_units\n", + " lunits = self.config.organic_property_package.get_metadata().get_derived_units\n", + " flow_basis = self.aqueous_phase.properties_out[t_init].get_material_flow_basis()\n", + "\n", + " if flow_basis == MaterialFlowBasis.mass:\n", + " fb = \"flow_mass\"\n", + " else:\n", + " raise ConfigurationError(\n", + " f\"{self.name} Liquid-Liquid Extractor only supports mass \"\n", + " f\"basis for MaterialFlowBasis.\"\n", + " )\n", + "\n", + " # Material balances\n", + " def rule_material_aq_balance(self, t, j):\n", + " if j in common_comps:\n", + " return self.aqueous_phase.mass_transfer_term[\n", + " t, \"Aq\", j\n", + " ] == -self.organic_phase.config.property_package.diffusion_factor[j] * (\n", + " self.aqueous_phase.properties_in[t].get_material_flow_terms(\"Aq\", j)\n", + " )\n", + " elif j in self.organic_phase.properties_out.component_list:\n", + " # No mass transfer term\n", + " # Set organic flowrate to an arbitrary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * lunits(fb)\n", + " elif j in self.aqueous_phase.properties_out.component_list:\n", + " # No mass transfer term\n", + " # Set aqueous flowrate to an arbitrary small value\n", + " return self.aqueous_phase.mass_transfer_term[t, \"Aq\", j] == 0 * aunits(fb)\n", + "\n", + " self.material_aq_balance = Constraint(\n", + " self.flowsheet().time,\n", + " self.aqueous_phase.properties_out.component_list,\n", + " rule=rule_material_aq_balance,\n", + " doc=\"Unit level material balances for Aq\",\n", + " )\n", + "\n", + " def rule_material_liq_balance(self, t, j):\n", + " if j in common_comps:\n", + " return (\n", + " self.organic_phase.mass_transfer_term[t, \"Org\", j]\n", + " == -self.aqueous_phase.mass_transfer_term[t, \"Aq\", j]\n", + " )\n", + " else:\n", + " # No mass transfer term\n", + " # Set organic flowrate to an arbitrary small value\n", + " return self.organic_phase.mass_transfer_term[t, \"Org\", j] == 0 * aunits(fb)\n", + "\n", + " self.material_org_balance = Constraint(\n", + " self.flowsheet().time,\n", + " self.organic_phase.properties_out.component_list,\n", + " rule=rule_material_liq_balance,\n", + " doc=\"Unit level material balances Org\",\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Initialization Routine\n", + "\n", + "After writing the unit model it is crucial to initialize the model properly, as non-linear models may encounter local minima or infeasibility if not initialized properly. IDAES provides us with a few initialization routines which may not work for all the models, and in such cases the developer will have to define their own initialization routines. \n", + "\n", + "To create a custom initialization routine, model developers must create an initialize method as part of their model, and provide a sequence of steps intended to build up a feasible solution. Initialization routines generally make use of Pyomo\u2019s tools for activating and deactivating constraints and often involve solving multiple sub-problems whilst building up an initial state.\n", + "\n", + "For this tutorial we would use the pre-defined initialization routine of `BlockTriangularizationInitializer` when initializing the model in the flowsheet. This Initializer should be suitable for most models, but may struggle to initialize\n", + "tightly coupled systems of equations. This method of initialization will follow the following workflow. \n", + "\n", + "- Have precheck for structural singularity\n", + "- Run incidence analysis on given block data and check matching.\n", + "- Call Block Triangularization solver on the model.\n", + "- Call solve_strongly_connected_components on a given BlockData.\n", + "\n", + "More details about this initialization routine can be found [here](https://github.com/IDAES/idaes-pse/blob/c09433b9afed5ae2fe25c0ccdc732783324f0101/idaes/core/initialization/block_triangularization.py). \n", + "\n", + "\n", + "This marks the conclusion of creating a custom unit model, for a more detailed explanation on creating a unit model refer [this resource](../../unit_models/custom_unit_models/custom_compressor_usr.ipynb). The next sections will deal with the diagnostics and testing of the property package and unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3.3 Building a Flowsheet\n", + "\n", + "Once we have set up the unit model and its property packages, we can start building a flowsheet using them. In this tutorial, we're focusing on a simple flowsheet with just a liquid-liquid extractor. To create the flowsheet we follow the following steps:\n", + "\n", + "- Import necessary libraries\n", + "- Create a Pyomo model.\n", + "- Inside the model, create a flowsheet block.\n", + "- Assign property packages to the flowsheet block.\n", + "- Add the liquid-liquid extractor to the flowsheet block.\n", + "- Fix variable to make it a square problem\n", + "- Run an initialization process.\n", + "- Solve the flowsheet.\n", + "\n", + "Following these steps, we've built a basic flowsheet using Pyomo. For more details, refer to the [documentation](../../flowsheets/hda_flowsheet_with_distillation_usr.ipynb).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "Ipopt 3.13.2: linear_solver=\"ma57\"\n", + "max_iter=200\n", + "nlp_scaling_method=\"gradient-based\"\n", + "tol=1e-08\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...: 6\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 0\n", + "\n", + "Total number of variables............................: 6\n", + " variables with only lower bounds: 6\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.98e+01 -1.0 4.10e-01 - 1.00e+00 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.05e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.97e+04 1.6 3.67e+02 - 1.00e+00 2.30e-03f 1\n", + " 5r 0.0000000e+00 4.00e+01 1.13e+02 1.6 3.89e-01 - 9.97e-01 1.00e+00f 1\n", + " 6r 0.0000000e+00 4.00e+01 1.13e+00 -1.2 8.42e-02 - 9.92e-01 9.87e-01f 1\n", + " 7r 0.0000000e+00 4.00e+01 9.78e-04 -4.3 8.94e-04 - 1.00e+00 1.00e+00f 1\n", + " 8r 0.0000000e+00 4.00e+01 5.79e-05 -6.4 1.49e-07 - 1.00e+00 1.00e+00f 1\n", + " 9r 0.0000000e+00 4.00e+01 5.20e-06 -9.0 6.12e-10 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 9\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 2.5739427655935287e-07 2.5739427655935287e-07\n", + "Constraint violation....: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "Complementarity.........: 9.0909090910996662e-10 9.0909090910996662e-10\n", + "Overall NLP error.......: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 15\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 15\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 12\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "Could not solve fs.lex after block triangularization finished.\n" + ] + } + ], + "source": [ + "import pyomo.environ as pyo\n", + "from idaes.core import FlowsheetBlock\n", + "\n", + "from idaes.core.initialization.block_triangularization import (\n", + " BlockTriangularizationInitializer,\n", + ")\n", + "from liquid_extraction.organic_property import OrgPhase\n", + "from liquid_extraction.aqueous_property import AqPhase\n", + "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", + "\n", + "\n", + "def build_model():\n", + " m = pyo.ConcreteModel()\n", + " m.fs = FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.lex = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + " return m\n", + "\n", + "\n", + "def fix_state_variables(m):\n", + " m.fs.lex.organic_inlet.flow_vol.fix(80 * pyo.units.L / pyo.units.hour)\n", + " m.fs.lex.organic_inlet.temperature.fix(300 * pyo.units.K)\n", + " m.fs.lex.organic_inlet.pressure.fix(1 * pyo.units.atm)\n", + " m.fs.lex.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", + " 1e-5 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", + " 1e-5 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", + " 1e-5 * pyo.units.g / pyo.units.L\n", + " )\n", + "\n", + " m.fs.lex.aqueous_inlet.flow_vol.fix(100 * pyo.units.L / pyo.units.hour)\n", + " m.fs.lex.aqueous_inlet.temperature.fix(300 * pyo.units.K)\n", + " m.fs.lex.aqueous_inlet.pressure.fix(1 * pyo.units.atm)\n", + " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(\n", + " 0.15 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(\n", + " 0.2 * pyo.units.g / pyo.units.L\n", + " )\n", + " m.fs.lex.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(\n", + " 0.1 * pyo.units.g / pyo.units.L\n", + " )\n", + "\n", + " return m\n", + "\n", + "\n", + "def initialize_model(m):\n", + " initializer = BlockTriangularizationInitializer()\n", + " try:\n", + " initializer.initialize(m.fs.lex)\n", + " except InitializationError as err:\n", + " print(err)\n", + " return m\n", + "\n", + "\n", + "def main():\n", + " m = build_model()\n", + " m = fix_state_variables(m)\n", + " m = initialize_model(m)\n", + " return m\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " main()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 4. Model Diagnostics using DiagnosticsToolbox\n", + "\n", + "Here, during initialization, we encounter warnings indicating that variables are being set to negative values before an exception is raised stating that solving the model failed. These issues suggest that there may be flaws in the model that require further investigation using the DiagnosticsToolbox from IDAES. A detailed notebook on using `DiagnosticsToolbox` can be found [here](../../diagnostics/degeneracy_hunter_usr.ipynb).\n", + "\n", + "To proceed with investigating these issues, we need to import the DiagnosticsToolbox. We can gain a better understanding of its functionality by running the help function on it. " + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "from idaes.core.util import DiagnosticsToolbox" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The help() function provides comprehensive information on the DiagnosticsToolbox and all its supported methods. However, it's essential to focus on the initial steps outlined at the beginning of the docstring to get started effectively.\n", + "\n", + "Here's a breakdown of the steps to start with:\n", + "\n", + "- `Instantiate Model:` Ensure you have an instance of the model with degrees of freedom equal to 0.\n", + "\n", + "- `Create DiagnosticsToolbox Instance:` Next, instantiate a DiagnosticsToolbox object.\n", + "\n", + "- `Provide Model to DiagnosticsToolbox:` Pass the model instance to the DiagnosticsToolbox.\n", + "\n", + "- `Call report_structural_issues() Function:` Finally, call the report_structural_issues() function. This function will highlight any warnings in the model's structure, such as unit inconsistencies or other issues related to variables in the caution section.\n", + "\n", + "By following these steps, you can efficiently utilize the DiagnosticsToolbox to identify and address any structural issues or warnings in your model." + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]' to a value\n", + "`-0.1725` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3]' to a value\n", + "`-0.4` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "WARNING (W1001): Setting Var\n", + "'fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4]' to a value\n", + "`-0.05` (float) not in domain NonNegativeReals.\n", + " See also https://pyomo.readthedocs.io/en/stable/errors.html#w1001\n", + "Ipopt 3.13.2: linear_solver=\"ma57\"\n", + "max_iter=200\n", + "nlp_scaling_method=\"gradient-based\"\n", + "tol=1e-08\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...: 6\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 0\n", + "\n", + "Total number of variables............................: 6\n", + " variables with only lower bounds: 6\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 6\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.98e+01 -1.0 4.10e-01 - 1.00e+00 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.05e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.97e+04 1.6 3.67e+02 - 1.00e+00 2.30e-03f 1\n", + " 5r 0.0000000e+00 4.00e+01 1.13e+02 1.6 3.89e-01 - 9.97e-01 1.00e+00f 1\n", + " 6r 0.0000000e+00 4.00e+01 1.13e+00 -1.2 8.42e-02 - 9.92e-01 9.87e-01f 1\n", + " 7r 0.0000000e+00 4.00e+01 9.78e-04 -4.3 8.94e-04 - 1.00e+00 1.00e+00f 1\n", + " 8r 0.0000000e+00 4.00e+01 5.79e-05 -6.4 1.49e-07 - 1.00e+00 1.00e+00f 1\n", + " 9r 0.0000000e+00 4.00e+01 5.20e-06 -9.0 6.12e-10 - 1.00e+00 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 9\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 2.5739427655935287e-07 2.5739427655935287e-07\n", + "Constraint violation....: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "Complementarity.........: 9.0909090910996662e-10 9.0909090910996662e-10\n", + "Overall NLP error.......: 3.9999999000000912e+01 3.9999999000000912e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 15\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 15\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 12\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 10\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.002\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "Could not solve fs.lex after block triangularization finished.\n", + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "1 Cautions\n", + "\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "m = main()\n", + "dt = DiagnosticsToolbox(m)\n", + "dt.report_structural_issues()" + ] }, { - "data": { - "text/plain": [ - "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.07779264450073242}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Although no warnings were reported, it's important to note that there are 3 variables fixed to 0 and 10 unused variables, out of which 4 are fixed. As indicated in the output, the next step is to solve the model. After solving, you should call the report_numerical_issues() function. This function will help identify any numerical issues that may arise during the solution process." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "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...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\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 4.10e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 4.00e+01 4.93e+01 -1.0 4.10e-01 - 9.91e-01 2.41e-02h 1\n", + " 2 0.0000000e+00 4.00e+01 2.03e+05 -1.0 4.00e-01 - 1.00e+00 2.47e-04h 1\n", + " 3r 0.0000000e+00 4.00e+01 1.00e+03 1.6 0.00e+00 - 0.00e+00 3.09e-07R 4\n", + " 4r 0.0000000e+00 4.00e+01 9.88e+04 1.6 3.68e+02 - 9.92e-01 2.29e-03f 1\n", + " 5r 0.0000000e+00 3.60e+01 3.03e+00 1.6 4.01e+00 - 1.00e+00 1.00e+00f 1\n", + " 6r 0.0000000e+00 3.69e+01 1.21e+01 -1.2 9.24e-01 - 9.69e-01 9.78e-01f 1\n", + " 7r 0.0000000e+00 3.70e+01 2.11e-01 -1.9 1.00e-01 - 9.97e-01 1.00e+00f 1\n", + " 8r 0.0000000e+00 3.78e+01 2.03e-02 -4.3 8.71e-01 - 9.71e-01 1.00e+00f 1\n", + " 9r 0.0000000e+00 3.80e+01 2.62e-04 -6.4 1.24e-01 - 9.99e-01 1.00e+00f 1\n", + "iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls\n", + " 10r 0.0000000e+00 3.81e+01 5.87e-09 -6.4 1.58e-01 - 1.00e+00 1.00e+00f 1\n", + " 11r 0.0000000e+00 3.91e+01 1.09e-05 -9.0 9.35e-01 - 9.68e-01 1.00e+00f 1\n", + "\n", + "Number of Iterations....: 11\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 5.1393961893966849e-07 5.1393961893966849e-07\n", + "Constraint violation....: 3.9105165554489545e+01 3.9105165554489545e+01\n", + "Complementarity.........: 9.0909090910996620e-10 9.0909090910996620e-10\n", + "Overall NLP error.......: 3.9105165554489545e+01 3.9105165554489545e+01\n", + "\n", + "\n", + "Number of objective function evaluations = 17\n", + "Number of objective gradient evaluations = 5\n", + "Number of equality constraint evaluations = 17\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 14\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 12\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Converged to a point of local infeasibility. Problem may be infeasible.\n", + "WARNING: Loading a SolverResults object with a warning status into\n", + "model.name=\"unknown\";\n", + " - termination condition: infeasible\n", + " - message from solver: Ipopt 3.13.2\\x3a Converged to a locally infeasible\n", + " point. Problem may be infeasible.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'warning', 'Message': 'Ipopt 3.13.2\\\\x3a Converged to a locally infeasible point. Problem may be infeasible.', 'Termination condition': 'infeasible', 'Id': 200, 'Error rc': 0, 'Time': 0.06122612953186035}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver = pyo.SolverFactory(\"ipopt\")\n", + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The model is probably infeasible, indicating numerical issues with the model. We should call the `report_numerical_issues()` function and check the constraints/variables causing this issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Jacobian Condition Number: 7.955E+03\n", + "\n", + "------------------------------------------------------------------------------------\n", + "2 WARNINGS\n", + "\n", + " WARNING: 6 Constraints with large residuals (>1.0E-05)\n", + " WARNING: 5 Variables at or outside bounds (tol=0.0E+00)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "3 Cautions\n", + "\n", + " Caution: 8 Variables with value close to their bounds (abs=1.0E-04, rel=1.0E-04)\n", + " Caution: 5 Variables with value close to zero (tol=1.0E-08)\n", + " Caution: 3 Variables with extreme value (<1.0E-04 or >1.0E+04)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " display_constraints_with_large_residuals()\n", + " compute_infeasibility_explanation()\n", + " display_variables_at_or_outside_bounds()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_numerical_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, it's observed that the condition number of the Jacobian is high, indicating that the Jacobian is ill-conditioned. Additionally, there are 2 warnings related to constraints with large residuals and variables at or outside the bounds. The cautions mentioned in the output are also related to these warnings.\n", + "\n", + "As suggested, the next steps would be to:\n", + "\n", + "- Call the `display_variables_at_or_outside_bounds()` function to investigate variables at or outside the bounds.\n", + "\n", + "- Call the `display_constraints_with_large_residuals()` function to examine constraints with large residuals.\n", + "\n", + "These steps will help identify the underlying causes of the numerical issues and constraints violations, allowing for further analysis and potential resolution. " + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following variable(s) have values at or outside their bounds (tol=0.0E+00):\n", + "\n", + " fs.lex.organic_phase.properties_in[0.0].pressure (fixed): value=1.0 bounds=(1, 5)\n", + " fs.lex.organic_phase.properties_out[0.0].pressure (free): value=1 bounds=(1, 5)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[KNO3] (free): value=0.0 bounds=(0, None)\n", + " fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[CaSO4] (free): value=0.0 bounds=(0, None)\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_variables_at_or_outside_bounds()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this scenario, there are a couple of issues to address:\n", + "\n", + "- The pressure variable is fixed to 1, which is its lower bound. This could potentially lead to numerical issues, although it may not affect the model significantly since there is no pressure change in the model. To mitigate this, consider adjusting the lower bound of the pressure variable to avoid having its value at or outside the bounds.\n", + "\n", + "- The more concerning issue is with the `conc_mass_comp` variable attempting to go below 0 in the output. This suggests that there may be constraints involving `conc_mass_comp` in the aqueous phase causing this behavior. To investigate further, it's recommended to call the `display_constraints_with_large_residuals()` function. This will provide insights into whether constraints involving `conc_mass_comp` are contributing to the convergence issue." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "The following constraint(s) have large residuals (>1.0E-05):\n", + "\n", + " fs.lex.material_aq_balance[0.0,NaCl]: 5.49716E-01\n", + " fs.lex.material_aq_balance[0.0,KNO3]: 8.94833E-01\n", + " fs.lex.material_aq_balance[0.0,CaSO4]: 5.48843E-02\n", + " fs.lex.aqueous_phase.material_balances[0.0,NaCl]: 1.67003E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,KNO3]: 3.91052E+01\n", + " fs.lex.aqueous_phase.material_balances[0.0,CaSO4]: 4.94512E+00\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.display_constraints_with_large_residuals()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As expected there are convergence issues with the constraints which have `conc_mass_comp` variable in them specifically in the aqueous phase. Now, let us investigate further by printing this constraints and checking the value of each term. Since this is an persistent issue across the components, we can focus on just one of the component to identify the issue. " + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_balances} : Material balances\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : (fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) - (fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_out[0.0].flow_vol) + fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.material_balances[0.0, \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solute_set, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.15 : None : True : True : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : True : True : NonNegativeReals\n", + "{Member of conc_mass_comp} : Component mass concentrations\n", + " Size=3, Index=fs.aq_properties.solute_set, Units=g/l\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " NaCl : 0 : 0.0 : None : False : False : NonNegativeReals\n", + "flow_vol : Total volumetric flowrate\n", + " Size=1, Index=None, Units=l/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " None : 0 : 100.0 : None : False : False : NonNegativeReals\n", + "{Member of mass_transfer_term} : Component material transfer into unit\n", + " Size=4, Index=fs._time*fs.aq_properties._phase_component_set, Units=g/h\n", + " Key : Lower : Value : Upper : Fixed : Stale : Domain\n", + " (0.0, 'Aq', 'NaCl') : None : -31.700284300098897 : None : False : False : Reals\n" + ] + } + ], + "source": [ + "m.fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_in[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].conc_mass_comp[\"NaCl\"].pprint()\n", + "m.fs.lex.aqueous_phase.properties_out[0.0].flow_vol.pprint()\n", + "m.fs.lex.aqueous_phase.mass_transfer_term[0.0, \"Aq\", \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It seems there is a discrepancy between the mass transfer term and the amount of input of NaCl. This can be inferred from the values where the input equals 15g/h and the `mass_transfer_term` equals -31.706g/h.\n", + "\n", + "To further investigate this issue, it's advisable to examine the `material_aq_balance` constraint within the unit model where the `mass_transfer_term` is defined. By printing out this constraint and analyzing its components, you can gain a better understanding of the discrepancy and take appropriate corrective actions." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{Member of material_aq_balance} : Unit level material balances for Aq\n", + " Size=4, Index=fs._time*fs.aq_properties.component_list, Active=True\n", + " Key : Lower : Body : Upper : Active\n", + " (0.0, 'NaCl') : 0.0 : fs.lex.aqueous_phase.mass_transfer_term[0.0,Aq,NaCl] + fs.org_properties.diffusion_factor[NaCl]*(fs.lex.aqueous_phase.properties_in[0.0].conc_mass_comp[NaCl]*fs.lex.aqueous_phase.properties_in[0.0].flow_vol) : 0.0 : True\n" + ] + } + ], + "source": [ + "m.fs.lex.material_aq_balance[0.0, \"NaCl\"].pprint()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here the problem can be tracked down easily as there being a typing error while recording the distribution factor. The distribution factor here was wrongly written ignoring its magnitude which should have been 1e-2, but that was missed, thus adjusting the distribution factor parameter we should have this issue resolved. " + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [], + "source": [ + "m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + ")\n", + "m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + ")\n", + "\n", + "m.fs.lex.organic_phase.properties_in[0.0].pressure.setlb(0.5)\n", + "m.fs.lex.organic_phase.properties_out[0.0].pressure.setlb(0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the corrective actions, we should check if this has made any structural issues, for this we would call `report_structural_issues()`" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "====================================================================================\n", + "Model Statistics\n", + "\n", + " Activated Blocks: 21 (Deactivated: 0)\n", + " Free Variables in Activated Constraints: 16 (External: 0)\n", + " Free Variables with only lower bounds: 8\n", + " Free Variables with only upper bounds: 0\n", + " Free Variables with upper and lower bounds: 0\n", + " Fixed Variables in Activated Constraints: 8 (External: 0)\n", + " Activated Equality Constraints: 16 (Deactivated: 0)\n", + " Activated Inequality Constraints: 0 (Deactivated: 0)\n", + " Activated Objectives: 0 (Deactivated: 0)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "0 WARNINGS\n", + "\n", + " No warnings found!\n", + "\n", + "------------------------------------------------------------------------------------\n", + "1 Cautions\n", + "\n", + " Caution: 10 unused variables (4 fixed)\n", + "\n", + "------------------------------------------------------------------------------------\n", + "Suggested next steps:\n", + "\n", + " Try to initialize/solve your model and then call report_numerical_issues()\n", + "\n", + "====================================================================================\n" + ] + } + ], + "source": [ + "dt.report_structural_issues()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now since there are no warnings we can go ahead and solve the model and see if the results are optimal. " + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "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...: 33\n", + "Number of nonzeros in inequality constraint Jacobian.: 0\n", + "Number of nonzeros in Lagrangian Hessian.............: 14\n", + "\n", + "Total number of variables............................: 16\n", + " variables with only lower bounds: 8\n", + " variables with lower and upper bounds: 0\n", + " variables with only upper bounds: 0\n", + "Total number of equality constraints.................: 16\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 5.85e+01 1.00e+00 -1.0 0.00e+00 - 0.00e+00 0.00e+00 0\n", + " 1 0.0000000e+00 3.55e-15 8.41e+00 -1.0 5.85e+01 - 1.05e-01 1.00e+00h 1\n", + "\n", + "Number of Iterations....: 1\n", + "\n", + " (scaled) (unscaled)\n", + "Objective...............: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Dual infeasibility......: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Constraint violation....: 3.5527136788005009e-15 3.5527136788005009e-15\n", + "Complementarity.........: 0.0000000000000000e+00 0.0000000000000000e+00\n", + "Overall NLP error.......: 3.5527136788005009e-15 3.5527136788005009e-15\n", + "\n", + "\n", + "Number of objective function evaluations = 2\n", + "Number of objective gradient evaluations = 2\n", + "Number of equality constraint evaluations = 2\n", + "Number of inequality constraint evaluations = 0\n", + "Number of equality constraint Jacobian evaluations = 2\n", + "Number of inequality constraint Jacobian evaluations = 0\n", + "Number of Lagrangian Hessian evaluations = 1\n", + "Total CPU secs in IPOPT (w/o function evaluations) = 0.000\n", + "Total CPU secs in NLP function evaluations = 0.000\n", + "\n", + "EXIT: Optimal Solution Found.\n" + ] + }, + { + "data": { + "text/plain": [ + "{'Problem': [{'Lower bound': -inf, 'Upper bound': inf, 'Number of objectives': 1, 'Number of constraints': 16, 'Number of variables': 16, 'Sense': 'unknown'}], 'Solver': [{'Status': 'ok', 'Message': 'Ipopt 3.13.2\\\\x3a Optimal Solution Found', 'Termination condition': 'optimal', 'Id': 0, 'Error rc': 0, 'Time': 0.0668952465057373}], 'Solution': [OrderedDict([('number of solutions', 0), ('number of solutions displayed', 0)])]}" + ] + }, + "execution_count": 50, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "solver.solve(m, tee=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is a good sign that the model solved optimally and a solution was found. \n", + "\n", + "**NOTE:** It is a good practice to run the model through DiagnosticsToolbox regardless of the solver termination status. \n", + "\n", + "The next section we shall focus on testing the unit model. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5. Testing\n", + "\n", + "Testing is a crucial part of model development to ensure that the model works as expected, and remains reliable. Here's an overview of why we conduct testing:\n", + "\n", + "1. `Verify Correctness`: Testing ensures that the model works as expected and meets the specified requirements. \n", + "2. `Detect Bugs and Issues`: Testing helps in identifying bugs, errors, or unexpected behaviors in the code or model, allowing for timely fixes.\n", + "3. `Ensure Reliability`: Testing improves the reliability and robustness of the software, reducing the risk of failures when the user uses it.\n", + "4. `Support Changes`: Tests provide confidence when making changes or adding new features, ensuring that existing functionalities are not affected and work as they should.\n", + "\n", + "There are typically 3 types of tests:\n", + "\n", + "1. `Unit tests`: Test runs quickly (under 2 seconds) and has no network/system dependencies. Uses only libraries installed by default with the software\n", + "2. `Component test`: Test may run more slowly (under 10 seconds, or so), e.g. it may run a solver or create a bunch of files. Like unit tests, it still shouldn't depend on special libraries or dependencies.\n", + "3. `Integration test`: Test may take a long time to run, and may have complex dependencies.\n", + "\n", + "The expectation is that unit tests should be run by developers rather frequently, component tests should be run by the continuous integration system before running code, and integration tests are run across the codebase regularly, but infrequently (e.g. daily).\n", + "\n", + "\n", + "As a developer, testing is a crucial aspect of ensuring the reliability and correctness of the unit model. The testing process involves both Unit tests and Component tests, and pytest is used as the testing framework. A typical test is marked with @pytest.mark.level, where the level indicates the depth or specificity of the testing. This is written in a file usually named as test_*.py or *_test.py. The test files have functions written in them with the appropriate level of test being conducted. \n", + "\n", + "For more detailed information on testing methodologies and procedures, developers are encouraged to refer to [this resource](https://idaes-pse.readthedocs.io/en/stable/reference_guides/developer/testing.html). The resource provides comprehensive guidance on the testing process and ensures that the unit model meets the required standards and functionality.\n", + "\n", + "## 5.1 Property package\n", + "### Unit Tests\n", + "\n", + "When writing tests for the Aqueous property phase package, it's essential to focus on key aspects to ensure the correctness and robustness of the implementation. Here are the areas to cover in the unit tests:\n", + "\n", + "1. Number of Config Dictionaries: Verify that the property phase package has the expected number of configuration dictionaries.\n", + "\n", + "2. State Block Class Name: Confirm that the correct state block class is associated with the Aqueous property phase package.\n", + "\n", + "3. Number of Phases: Check that the Aqueous property phase package defines the expected number of phases.\n", + "\n", + "4. Components in the Phase and Physical Parameter Values: Test that the components present in the Aqueous phase match the anticipated list. Additionally, validate that the physical parameter values (such as density, viscosity, etc.) are correctly defined.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "import pytest\n", + "from pyomo.environ import ConcreteModel, Param, value, Var\n", + "from pyomo.util.check_units import assert_units_consistent\n", + "from idaes.core import MaterialBalanceType, EnergyBalanceType\n", + "\n", + "from liquid_extraction.organic_property import OrgPhase\n", + "from liquid_extraction.aqueous_property import AqPhase\n", + "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", + "from idaes.core.solvers import get_solver\n", + "\n", + "solver = get_solver()\n", + "\n", + "\n", + "class TestParamBlock(object):\n", + " @pytest.fixture(scope=\"class\")\n", + " def model(self):\n", + " model = ConcreteModel()\n", + " model.params = AqPhase()\n", + " return model\n", + "\n", + " @pytest.mark.unit\n", + " def test_config(self, model):\n", + " assert len(model.params.config) == 1\n", + "\n", + " @pytest.mark.unit\n", + " def test_build(self, model):\n", + " assert len(model.params.phase_list) == 1\n", + " for i in model.params.phase_list:\n", + " assert i == \"Aq\"\n", + "\n", + " assert len(model.params.component_list) == 4\n", + " for i in model.params.component_list:\n", + " assert i in [\"H2O\", \"NaCl\", \"KNO3\", \"CaSO4\"]\n", + "\n", + " assert isinstance(model.params.cp_mass, Param)\n", + " assert value(model.params.cp_mass) == 4182\n", + "\n", + " assert isinstance(model.params.dens_mass, Param)\n", + " assert value(model.params.dens_mass) == 997\n", + "\n", + " assert isinstance(model.params.temperature_ref, Param)\n", + " assert value(model.params.temperature_ref) == 298.15" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next set of unit tests focuses on testing the build function in the state block. Here are the key aspects to cover in these tests:\n", + "\n", + "1. Existence and Initialized Values of State Variables: Verify that the state variables are correctly defined and initialized within the state block. This ensures that the state block is properly constructed and ready for initialization.\n", + "\n", + "2. Initialization Function Test: Check that state variables are not fixed before initialization and are released after initialization. This test ensures that the initialization process occurs as expected and that the state variables are appropriately managed throughout.\n", + "\n", + "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the aqueous property phase package. Similar tests can be written for the organic property package to ensure consistency and reliability across both packages." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [], + "source": [ + "class TestStateBlock(object):\n", + " @pytest.fixture(scope=\"class\")\n", + " def model(self):\n", + " model = ConcreteModel()\n", + " model.params = AqPhase()\n", + "\n", + " model.props = model.params.build_state_block([1])\n", + "\n", + " return model\n", + "\n", + " @pytest.mark.unit\n", + " def test_build(self, model):\n", + " assert isinstance(model.props[1].flow_vol, Var)\n", + " assert value(model.props[1].flow_vol) == 1\n", + "\n", + " assert isinstance(model.props[1].temperature, Var)\n", + " assert value(model.props[1].temperature) == 300\n", + "\n", + " assert isinstance(model.props[1].conc_mass_comp, Var)\n", + " assert len(model.props[1].conc_mass_comp) == 3\n", + "\n", + " @pytest.mark.unit\n", + " def test_initialize(self, model):\n", + " assert not model.props[1].flow_vol.fixed\n", + " assert not model.props[1].temperature.fixed\n", + " assert not model.props[1].pressure.fixed\n", + " for i in model.props[1].conc_mass_comp:\n", + " assert not model.props[1].conc_mass_comp[i].fixed\n", + "\n", + " model.props.initialize(hold_state=False, outlvl=1)\n", + "\n", + " assert not model.props[1].flow_vol.fixed\n", + " assert not model.props[1].temperature.fixed\n", + " assert not model.props[1].pressure.fixed\n", + " for i in model.props[1].conc_mass_comp:\n", + " assert not model.props[1].conc_mass_comp[i].fixed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Component Tests\n", + "In the component test, we aim to ensure unit consistency across the entire property package. Unlike unit tests that focus on individual functions, component tests assess the coherence and consistency of the entire package. Here's what the component test will entail:\n", + "\n", + "Unit Consistency Check: Verify that all units used within the property package are consistent throughout. This involves checking that all parameters, variables, and equations within the package adhere to the same unit system, ensuring compatibility.\n", + "\n", + "By conducting a comprehensive component test, we can ensure that the property package functions as a cohesive unit, maintaining consistency and reliability across its entirety. This concludes our tests on the property package. Next we shall test the unit model. " + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [], + "source": [ + "@pytest.mark.component\n", + "def check_units(model):\n", + " model = ConcreteModel()\n", + " model.params = AqPhase()\n", + " assert_units_consistent(model)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 5.2 Unit Model\n", + "### Unit tests\n", + "Unit tests for the unit model encompass verifying the configuration arguments and the build function, similar to the approach taken for the property package. When testing the config arguments, we ensure that the correct number of arguments is provided and then match each argument with the expected one. This ensures that the unit model is properly configured and ready to operate as intended." + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [], + "source": [ + "import pytest\n", + "\n", + "from idaes.core import FlowsheetBlock\n", + "import idaes.models.unit_models\n", + "from idaes.core.solvers import get_solver\n", + "import idaes.logger as idaeslog\n", + "\n", + "\n", + "from pyomo.environ import value, check_optimal_termination, units\n", + "from pyomo.util.check_units import assert_units_consistent\n", + "from idaes.core.util.model_statistics import (\n", + " number_variables,\n", + " number_total_constraints,\n", + ")\n", + "from idaes.core.solvers import get_solver\n", + "from idaes.core.initialization import (\n", + " SingleControlVolumeUnitInitializer,\n", + ")\n", + "\n", + "solver = get_solver()\n", + "\n", + "\n", + "@pytest.mark.unit\n", + "def test_config():\n", + " m = ConcreteModel()\n", + " m.fs = FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.unit = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + "\n", + " # Check unit config arguments\n", + " assert len(m.fs.unit.config) == 9\n", + "\n", + " # Check for config arguments\n", + " assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault\n", + " assert not m.fs.unit.config.has_pressure_change\n", + " assert not m.fs.unit.config.has_phase_equilibrium\n", + " assert m.fs.unit.config.organic_property_package is m.fs.org_properties\n", + " assert m.fs.unit.config.aqueous_property_package is m.fs.aq_properties\n", + "\n", + " # Check for unit initializer\n", + " assert m.fs.unit.default_initializer is SingleControlVolumeUnitInitializer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In testing the build function, we verify whether the number of variables aligns with the intended values and also check for the existence of desired constraints within the unit model. This ensures that the unit model is constructed accurately and includes all the necessary variables and constraints required for its proper functioning." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [], + "source": [ + "class TestBuild(object):\n", + " @pytest.fixture(scope=\"class\")\n", + " def model(self):\n", + " m = ConcreteModel()\n", + " m.fs = FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.unit = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + "\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.l / units.h)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.l)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.l)\n", + "\n", + " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.l / units.h)\n", + " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.l)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.l)\n", + "\n", + " return m\n", + "\n", + " @pytest.mark.build\n", + " @pytest.mark.unit\n", + " def test_build(self, model):\n", + "\n", + " assert hasattr(model.fs.unit, \"aqueous_inlet\")\n", + " assert len(model.fs.unit.aqueous_inlet.vars) == 4\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.aqueous_inlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"organic_inlet\")\n", + " assert len(model.fs.unit.organic_inlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_inlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_inlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"aqueous_outlet\")\n", + " assert len(model.fs.unit.aqueous_outlet.vars) == 4\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.aqueous_outlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"organic_outlet\")\n", + " assert len(model.fs.unit.organic_outlet.vars) == 4\n", + " assert hasattr(model.fs.unit.organic_outlet, \"flow_vol\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"conc_mass_comp\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"temperature\")\n", + " assert hasattr(model.fs.unit.organic_outlet, \"pressure\")\n", + "\n", + " assert hasattr(model.fs.unit, \"material_aq_balance\")\n", + " assert hasattr(model.fs.unit, \"material_org_balance\")\n", + "\n", + " assert number_variables(model) == 34\n", + " assert number_total_constraints(model) == 16" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Component tests\n", + "\n", + "During the component tests, we evaluate the performance of the unit model when integrated with the property package. This evaluation process typically involves several steps:\n", + "\n", + "1. Unit Consistency Check: Verify that the unit model maintains consistency in its units throughout the model. This ensures that all variables and constraints within the model adhere to the same unit system, guaranteeing compatibility.\n", + "\n", + "2. Termination Condition Verification: This involves checking whether the model terminates optimally with the given inlet conditions.\n", + "\n", + "3. Variable Value Assessment: Check the values of outlet variables against the expected values. To account for the numerical tolerance of the solvers, the values are compared using the approx function with a relative tolerance.\n", + "\n", + "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered.\n", + "\n", + "5. Structural Issues: Verify that there are no structural issues with the model. \n", + "\n", + "By performing these checks, we conclude the testing for the unit model. " + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "class TestFlowsheet:\n", + " @pytest.fixture\n", + " def model(self):\n", + " m = ConcreteModel()\n", + " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", + " m.fs.org_properties = OrgPhase()\n", + " m.fs.aq_properties = AqPhase()\n", + "\n", + " m.fs.unit = LiqExtraction(\n", + " dynamic=False,\n", + " has_pressure_change=False,\n", + " organic_property_package=m.fs.org_properties,\n", + " aqueous_property_package=m.fs.aq_properties,\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", + " )\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", + " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", + " )\n", + "\n", + " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", + " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", + " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", + "\n", + " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", + " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", + " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.kg)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.kg)\n", + " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.kg)\n", + "\n", + " return m\n", + "\n", + " @pytest.mark.component\n", + " def test_unit_model(self, model):\n", + " assert_units_consistent(model)\n", + " solver = get_solver()\n", + " results = solver.solve(model, tee=False)\n", + "\n", + " # Check for optimal termination\n", + " assert check_optimal_termination(results)\n", + "\n", + " # Checking for outlet flows\n", + " assert value(model.fs.unit.organic_outlet.flow_vol[0]) == pytest.approx(\n", + " 80.0, rel=1e-5\n", + " )\n", + " assert value(model.fs.unit.aqueous_outlet.flow_vol[0]) == pytest.approx(\n", + " 10.0, rel=1e-5\n", + " )\n", + "\n", + " # Checking for outlet mass_comp\n", + " assert value(\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"CaSO4\"]\n", + " ) == pytest.approx(0.000187499, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"KNO3\"]\n", + " ) == pytest.approx(0.000749999, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.organic_outlet.conc_mass_comp[0, \"NaCl\"]\n", + " ) == pytest.approx(0.000403124, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"CaSO4\"]\n", + " ) == pytest.approx(0.0985, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"KNO3\"]\n", + " ) == pytest.approx(0.194, rel=1e-5)\n", + " assert value(\n", + " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"NaCl\"]\n", + " ) == pytest.approx(0.146775, rel=1e-5)\n", + "\n", + " # Checking for outlet temperature\n", + " assert value(model.fs.unit.organic_outlet.temperature[0]) == pytest.approx(\n", + " 300, rel=1e-5\n", + " )\n", + " assert value(model.fs.unit.aqueous_outlet.temperature[0]) == pytest.approx(\n", + " 300, rel=1e-5\n", + " )\n", + "\n", + " # Checking for outlet pressure\n", + " assert value(model.fs.unit.organic_outlet.pressure[0]) == pytest.approx(\n", + " 1, rel=1e-5\n", + " )\n", + " assert value(model.fs.unit.aqueous_outlet.pressure[0]) == pytest.approx(\n", + " 1, rel=1e-5\n", + " )\n", + "\n", + " # Fixed state variables\n", + " assert model.fs.unit.organic_inlet.flow_vol[0].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", + " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", + " assert model.fs.unit.organic_inlet.temperature[0].fixed\n", + " assert model.fs.unit.organic_inlet.pressure[0].fixed\n", + "\n", + " assert model.fs.unit.aqueous_inlet.flow_vol[0].fixed\n", + " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", + " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", + " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", + " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", + " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", + "\n", + " @pytest.mark.component\n", + " def test_structural_issues(self, model):\n", + " dt = DiagnosticsToolbox(model)\n", + " dt.assert_no_structural_warnings()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this tutorial, we have covered the comprehensive process of creating a custom unit model from scratch. Let's recap the key steps we have undertaken:\n", + "\n", + "- Developing property package\n", + "- Constructing the unit model \n", + "- Creating a Flowsheet\n", + "- Debugging the model using DiagnosticsToolbox\n", + "- Writing tests for the unit model\n", + "\n", + "By following the aforementioned procedure, one can create their own custom unit model. This concludes the tutorial on creating a custom unit model. " ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" } - ], - "source": [ - "solver.solve(m, tee=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This is a good sign that the model solved optimally and a solution was found. \n", - "\n", - "**NOTE:** It is a good practice to run the model through DiagnosticsToolbox regardless of the solver termination status. \n", - "\n", - "The next section we shall focus on testing the unit model. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 5. Testing\n", - "\n", - "Testing is a crucial part of model development to ensure that the model works as expected, and remains reliable. Here's an overview of why we conduct testing:\n", - "\n", - "1. `Verify Correctness`: Testing ensure that the model works as expected and meets the specified requirements. \n", - "2. `Detect Bugs and Issues`: Testing helps in identifying bugs, errors, or unexpected behaviors in the code or model, allowing for timely fixes.\n", - "3. `Ensure Reliability`: Testing improves the reliability and robustness of the software, reducing the risk of failures when the user uses it.\n", - "4. `Support Changes`: Tests provide confidence when making changes or adding new features, ensuring that existing functionalities are not affected and work as they should.\n", - "\n", - "There are typically 3 types of tests:\n", - "\n", - "1. `Unit tests`: Test runs quickly (under 2 seconds) and has no network/system dependencies. Uses only libraries installed by default with the software\n", - "2. `Component test`: Test may run more slowly (under 10 seconds, or so), e.g. it may run a solver or create a bunch of files. Like unit tests, it still shouldn't depend on special libraries or dependencies.\n", - "3. `Integration test`: Test may take a long time to run, and may have complex dependencies.\n", - "\n", - "The expectation is that unit tests should be run by developers rather frequently, component tests should be run by the continuous integration system before running code, and integration tests are run across the codebase regularly, but infrequently (e.g. daily).\n", - "\n", - "\n", - "As a developer, testing is a crucial aspect of ensuring the reliability and correctness of the unit model. The testing process involves both Unit tests and Component tests, and pytest is used as the testing framework. A typical test is marked with @pytest.mark.level, where the level indicates the depth or specificity of the testing. This is written in a file usually named as test_*.py or *_test.py. The test files have functions written in them with the appropriate level of test being conducted. \n", - "\n", - "For more detailed information on testing methodologies and procedures, developers are encouraged to refer to [this resource](https://idaes-pse.readthedocs.io/en/stable/reference_guides/developer/testing.html). The resource provides comprehensive guidance on the testing process and ensures that the unit model meets the required standards and functionality.\n", - "\n", - "## 5.1 Property package\n", - "### Unit Tests\n", - "\n", - "When writing tests for the Aqueous property phase package, it's essential to focus on key aspects to ensure the correctness and robustness of the implementation. Here are the areas to cover in the unit tests:\n", - "\n", - "1. Number of Config Dictionaries: Verify that the property phase package has the expected number of configuration dictionaries.\n", - "\n", - "2. State Block Class Name: Confirm that the correct state block class is associated with the Aqueous property phase package.\n", - "\n", - "3. Number of Phases: Check that the Aqueous property phase package defines the expected number of phases.\n", - "\n", - "4. Components in the Phase and Physical Parameter Values: Test that the components present in the Aqueous phase match the anticipated list. Additionally, validate that the physical parameter values (such as density, viscosity, etc.) are correctly defined.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": {}, - "outputs": [], - "source": [ - "import pytest\n", - "from pyomo.environ import ConcreteModel, Param, value, Var\n", - "from pyomo.util.check_units import assert_units_consistent\n", - "from idaes.core import MaterialBalanceType, EnergyBalanceType\n", - "\n", - "from liquid_extraction.organic_property import OrgPhase\n", - "from liquid_extraction.aqueous_property import AqPhase\n", - "from liquid_extraction.liquid_liquid_extractor import LiqExtraction\n", - "from idaes.core.solvers import get_solver\n", - "\n", - "solver = get_solver()\n", - "\n", - "\n", - "class TestParamBlock(object):\n", - " @pytest.fixture(scope=\"class\")\n", - " def model(self):\n", - " model = ConcreteModel()\n", - " model.params = AqPhase()\n", - " return model\n", - "\n", - " @pytest.mark.unit\n", - " def test_config(self, model):\n", - " assert len(model.params.config) == 1\n", - "\n", - " @pytest.mark.unit\n", - " def test_build(self, model):\n", - " assert len(model.params.phase_list) == 1\n", - " for i in model.params.phase_list:\n", - " assert i == \"Aq\"\n", - "\n", - " assert len(model.params.component_list) == 4\n", - " for i in model.params.component_list:\n", - " assert i in [\"H2O\", \"NaCl\", \"KNO3\", \"CaSO4\"]\n", - "\n", - " assert isinstance(model.params.cp_mass, Param)\n", - " assert value(model.params.cp_mass) == 4182\n", - "\n", - " assert isinstance(model.params.dens_mass, Param)\n", - " assert value(model.params.dens_mass) == 997\n", - "\n", - " assert isinstance(model.params.temperature_ref, Param)\n", - " assert value(model.params.temperature_ref) == 298.15" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next set of unit tests focuses on testing the build function in the state block. Here are the key aspects to cover in these tests:\n", - "\n", - "1. Existence and Initialized Values of State Variables: Verify that the state variables are correctly defined and initialized within the state block. This ensures that the state block is properly constructed and ready for initialization.\n", - "\n", - "2. Initialization Function Test: Check that state variables are not fixed before initialization and are released after initialization. This test ensures that the initialization process occurs as expected and that the state variables are appropriately managed throughout.\n", - "\n", - "These unit tests provide comprehensive coverage for validating the functionality and behavior of the state block in the Aqueous property phase package. Similar tests can be written for the organic property package to ensure consistency and reliability across both packages." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": {}, - "outputs": [], - "source": [ - "class TestStateBlock(object):\n", - " @pytest.fixture(scope=\"class\")\n", - " def model(self):\n", - " model = ConcreteModel()\n", - " model.params = AqPhase()\n", - "\n", - " model.props = model.params.build_state_block([1])\n", - "\n", - " return model\n", - "\n", - " @pytest.mark.unit\n", - " def test_build(self, model):\n", - " assert isinstance(model.props[1].flow_vol, Var)\n", - " assert value(model.props[1].flow_vol) == 1\n", - "\n", - " assert isinstance(model.props[1].temperature, Var)\n", - " assert value(model.props[1].temperature) == 300\n", - "\n", - " assert isinstance(model.props[1].conc_mass_comp, Var)\n", - " assert len(model.props[1].conc_mass_comp) == 3\n", - "\n", - " @pytest.mark.unit\n", - " def test_initialize(self, model):\n", - " assert not model.props[1].flow_vol.fixed\n", - " assert not model.props[1].temperature.fixed\n", - " assert not model.props[1].pressure.fixed\n", - " for i in model.props[1].conc_mass_comp:\n", - " assert not model.props[1].conc_mass_comp[i].fixed\n", - "\n", - " model.props.initialize(hold_state=False, outlvl=1)\n", - "\n", - " assert not model.props[1].flow_vol.fixed\n", - " assert not model.props[1].temperature.fixed\n", - " assert not model.props[1].pressure.fixed\n", - " for i in model.props[1].conc_mass_comp:\n", - " assert not model.props[1].conc_mass_comp[i].fixed" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Component Tests\n", - "In the component test, we aim to ensure unit consistency across the entire property package. Unlike unit tests that focus on individual functions, component tests assess the coherence and consistency of the entire package. Here's what the component test will entail:\n", - "\n", - "Unit Consistency Check: Verify that all units used within the property package are consistent throughout. This involves checking that all parameters, variables, and equations within the package adhere to the same unit system, ensuring compatibility.\n", - "\n", - "By conducting a comprehensive component test, we can ensure that the property package functions as a cohesive unit, maintaining consistency and reliability across its entirety. This concludes our tests on the property package. Next we shall test the unit model. " - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "@pytest.mark.component\n", - "def check_units(model):\n", - " model = ConcreteModel()\n", - " model.params = AqPhase()\n", - " assert_units_consistent(model)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 5.2 Unit Model\n", - "### Unit tests\n", - "Unit tests for the unit model encompass verifying the configuration arguments and the build function, similar to the approach taken for the property package. When testing the config arguments, we ensure that the correct number of arguments is provided and then match each argument with the expected one. This ensures that the unit model is properly configured and ready to operate as intended." - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": {}, - "outputs": [], - "source": [ - "import pytest\n", - "\n", - "import idaes.core\n", - "import idaes.models.unit_models\n", - "from idaes.core.solvers import get_solver\n", - "import idaes.logger as idaeslog\n", - "\n", - "\n", - "from pyomo.environ import value, check_optimal_termination, units\n", - "from pyomo.util.check_units import assert_units_consistent\n", - "from idaes.core.util.model_statistics import (\n", - " number_variables,\n", - " number_total_constraints,\n", - ")\n", - "from idaes.core.solvers import get_solver\n", - "from idaes.core.initialization import (\n", - " SingleControlVolumeUnitInitializer,\n", - ")\n", - "\n", - "solver = get_solver()\n", - "\n", - "\n", - "@pytest.mark.unit\n", - "def test_config():\n", - " m = ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.unit = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - "\n", - " # Check unit config arguments\n", - " assert len(m.fs.unit.config) == 9\n", - "\n", - " # Check for config arguments\n", - " assert m.fs.unit.config.material_balance_type == MaterialBalanceType.useDefault\n", - " assert not m.fs.unit.config.has_pressure_change\n", - " assert not m.fs.unit.config.has_phase_equilibrium\n", - " assert m.fs.unit.config.organic_property_package is m.fs.org_properties\n", - " assert m.fs.unit.config.aqueous_property_package is m.fs.aq_properties\n", - "\n", - " # Check for unit initializer\n", - " assert m.fs.unit.default_initializer is SingleControlVolumeUnitInitializer" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In testing the build function, we verify whether the number of variables aligns with the intended values and also check for the existence of desired constraints within the unit model. This ensures that the unit model is constructed accurately and includes all the necessary variables and constraints required for its proper functioning." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "metadata": {}, - "outputs": [], - "source": [ - "class TestBuild(object):\n", - " @pytest.fixture(scope=\"class\")\n", - " def model(self):\n", - " m = ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.unit = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - "\n", - " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.l / units.h)\n", - " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.l)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.l)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.l)\n", - "\n", - " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.l / units.h)\n", - " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.l)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.l)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.l)\n", - "\n", - " return m\n", - "\n", - " @pytest.mark.build\n", - " @pytest.mark.unit\n", - " def test_build(self, model):\n", - "\n", - " assert hasattr(model.fs.unit, \"aqueous_inlet\")\n", - " assert len(model.fs.unit.aqueous_inlet.vars) == 4\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.aqueous_inlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"organic_inlet\")\n", - " assert len(model.fs.unit.organic_inlet.vars) == 4\n", - " assert hasattr(model.fs.unit.organic_inlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.organic_inlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.organic_inlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.organic_inlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"aqueous_outlet\")\n", - " assert len(model.fs.unit.aqueous_outlet.vars) == 4\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.aqueous_outlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"organic_outlet\")\n", - " assert len(model.fs.unit.organic_outlet.vars) == 4\n", - " assert hasattr(model.fs.unit.organic_outlet, \"flow_vol\")\n", - " assert hasattr(model.fs.unit.organic_outlet, \"conc_mass_comp\")\n", - " assert hasattr(model.fs.unit.organic_outlet, \"temperature\")\n", - " assert hasattr(model.fs.unit.organic_outlet, \"pressure\")\n", - "\n", - " assert hasattr(model.fs.unit, \"material_aq_balance\")\n", - " assert hasattr(model.fs.unit, \"material_org_balance\")\n", - "\n", - " assert number_variables(model) == 34\n", - " assert number_total_constraints(model) == 16" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Component tests\n", - "\n", - "During the component tests, we evaluate the performance of the unit model when integrated with the property package. This evaluation process typically involves several steps:\n", - "\n", - "1. Unit Consistency Check: Verify that the unit model maintains consistency in its units throughout the model. This ensures that all variables and constraints within the model adhere to the same unit system, guaranteeing compatibility.\n", - "\n", - "2. Termination Condition Verification: This involves checking whether the model terminates optimally with the given inlet conditions.\n", - "\n", - "3. Variable Value Assessment: Check the values of outlet variables against the expected values. To account for the numerical tolerance of the solvers, the values are compared using the approx function with a relative tolerance.\n", - "\n", - "4. Input Variable Stability Test: Verify that input variables, which should remain fixed during model operation, are not inadvertently unfixed or altered.\n", - "\n", - "5. Structural Issues: Verify that there are no structural issues with the model. \n", - "\n", - "By performing these checks, we conclude the testing for the unit model. " - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "metadata": {}, - "outputs": [], - "source": [ - "class TestFlowsheet:\n", - " @pytest.fixture\n", - " def model(self):\n", - " m = ConcreteModel()\n", - " m.fs = idaes.core.FlowsheetBlock(dynamic=False)\n", - " m.fs.org_properties = OrgPhase()\n", - " m.fs.aq_properties = AqPhase()\n", - "\n", - " m.fs.unit = LiqExtraction(\n", - " dynamic=False,\n", - " has_pressure_change=False,\n", - " organic_property_package=m.fs.org_properties,\n", - " aqueous_property_package=m.fs.aq_properties,\n", - " )\n", - " m.fs.org_properties.diffusion_factor[\"NaCl\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"NaCl\"] / 100\n", - " )\n", - " m.fs.org_properties.diffusion_factor[\"KNO3\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"KNO3\"] / 100\n", - " )\n", - " m.fs.org_properties.diffusion_factor[\"CaSO4\"] = (\n", - " m.fs.org_properties.diffusion_factor[\"CaSO4\"] / 100\n", - " )\n", - "\n", - " m.fs.unit.organic_inlet.flow_vol.fix(80 * units.ml / units.min)\n", - " m.fs.unit.organic_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.organic_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fix(0 * units.g / units.kg)\n", - " m.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0 * units.g / units.kg)\n", - "\n", - " m.fs.unit.aqueous_inlet.flow_vol.fix(10 * units.ml / units.min)\n", - " m.fs.unit.aqueous_inlet.temperature.fix(300 * units.K)\n", - " m.fs.unit.aqueous_inlet.pressure.fix(1 * units.atm)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fix(0.15 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fix(0.2 * units.g / units.kg)\n", - " m.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fix(0.1 * units.g / units.kg)\n", - "\n", - " return m\n", - "\n", - " @pytest.mark.component\n", - " def test_unit_model(self, model):\n", - " assert_units_consistent(model)\n", - " solver = get_solver()\n", - " results = solver.solve(model, tee=False)\n", - "\n", - " # Check for optimal termination\n", - " assert check_optimal_termination(results)\n", - "\n", - " # Checking for outlet flows\n", - " assert value(model.fs.unit.organic_outlet.flow_vol[0]) == pytest.approx(\n", - " 80.0, rel=1e-5\n", - " )\n", - " assert value(model.fs.unit.aqueous_outlet.flow_vol[0]) == pytest.approx(\n", - " 10.0, rel=1e-5\n", - " )\n", - "\n", - " # Checking for outlet mass_comp\n", - " assert value(\n", - " model.fs.unit.organic_outlet.conc_mass_comp[0, \"CaSO4\"]\n", - " ) == pytest.approx(0.000187499, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.organic_outlet.conc_mass_comp[0, \"KNO3\"]\n", - " ) == pytest.approx(0.000749999, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.organic_outlet.conc_mass_comp[0, \"NaCl\"]\n", - " ) == pytest.approx(0.000403124, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"CaSO4\"]\n", - " ) == pytest.approx(0.0985, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"KNO3\"]\n", - " ) == pytest.approx(0.194, rel=1e-5)\n", - " assert value(\n", - " model.fs.unit.aqueous_outlet.conc_mass_comp[0, \"NaCl\"]\n", - " ) == pytest.approx(0.146775, rel=1e-5)\n", - "\n", - " # Checking for outlet temperature\n", - " assert value(model.fs.unit.organic_outlet.temperature[0]) == pytest.approx(\n", - " 300, rel=1e-5\n", - " )\n", - " assert value(model.fs.unit.aqueous_outlet.temperature[0]) == pytest.approx(\n", - " 300, rel=1e-5\n", - " )\n", - "\n", - " # Checking for outlet pressure\n", - " assert value(model.fs.unit.organic_outlet.pressure[0]) == pytest.approx(\n", - " 1, rel=1e-5\n", - " )\n", - " assert value(model.fs.unit.aqueous_outlet.pressure[0]) == pytest.approx(\n", - " 1, rel=1e-5\n", - " )\n", - "\n", - " # Fixed state variables\n", - " assert model.fs.unit.organic_inlet.flow_vol[0].fixed\n", - " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", - " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", - " assert model.fs.unit.organic_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", - " assert model.fs.unit.organic_inlet.temperature[0].fixed\n", - " assert model.fs.unit.organic_inlet.pressure[0].fixed\n", - "\n", - " assert model.fs.unit.aqueous_inlet.flow_vol[0].fixed\n", - " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"NaCl\"].fixed\n", - " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"KNO3\"].fixed\n", - " assert model.fs.unit.aqueous_inlet.conc_mass_comp[0, \"CaSO4\"].fixed\n", - " assert model.fs.unit.aqueous_inlet.temperature[0].fixed\n", - " assert model.fs.unit.aqueous_inlet.pressure[0].fixed\n", - "\n", - " @pytest.mark.component\n", - " def test_structural_issues(self, model):\n", - " dt = DiagnosticsToolbox(model)\n", - " dt.assert_no_structural_warnings()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In this tutorial, we have covered the comprehensive process of creating a custom unit model from scratch. Let's recap the key steps we have undertaken:\n", - "\n", - "- Developing property package\n", - "- Constructing the unit model \n", - "- Creating a Flowsheet\n", - "- Debugging the model using DiagnosticsToolbox\n", - "- Writing tests for the unit model\n", - "\n", - "By following the aforementioned procedure, one can create their own custom unit model. This would conclude the tutorial on creating custom unit model. " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "idaes-pse", - "language": "python", - "name": "python3" + ], + "metadata": { + "kernelspec": { + "display_name": "examples-310-new", + "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.10.19" + } }, - "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.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 3 -} + "nbformat": 4, + "nbformat_minor": 3 +} \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index f8255530..10fdf4fa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ --editable .[dev,omlt] -idaes-pse @ git+https://github.com/IDAES/idaes-pse@main +# idaes-pse @ git+https://github.com/IDAES/idaes-pse@main # if you want to install idaes-pse from a PR instead of the main branch, # uncomment the line below replacing XYZ with the PR number # idaes-pse @ git+https://github.com/IDAES/idaes-pse@refs/pull/XYZ/merge +idaes-pse @ git+https://github.com/IDAES/idaes-pse@refs/pull/1704/merge \ No newline at end of file