diff --git a/idaes_examples/notebooks/docs/param_est/parameter_estimation_nrtl_using_state_block_test.ipynb b/idaes_examples/notebooks/docs/param_est/parameter_estimation_nrtl_using_state_block_test.ipynb
index 7d253335..a221dc46 100644
--- a/idaes_examples/notebooks/docs/param_est/parameter_estimation_nrtl_using_state_block_test.ipynb
+++ b/idaes_examples/notebooks/docs/param_est/parameter_estimation_nrtl_using_state_block_test.ipynb
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
- "execution_count": 10,
+ "execution_count": null,
"metadata": {
"tags": [
"header",
@@ -45,8 +45,7 @@
"\n",
"## Key links to documentation:\n",
"* NRTL Model - https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/property_models/activity_coefficient.html\n",
- "* parmest - https://pyomo.readthedocs.io/en/stable/contributed_packages/parmest/index.html\n",
- ""
+ "* parmest - https://pyomo.readthedocs.io/en/stable/contributed_packages/parmest/index.html\n"
]
},
{
@@ -61,7 +60,7 @@
},
{
"cell_type": "code",
- "execution_count": 12,
+ "execution_count": null,
"metadata": {
"tags": [
"solution"
@@ -85,7 +84,7 @@
},
{
"cell_type": "code",
- "execution_count": 13,
+ "execution_count": null,
"metadata": {
"tags": []
},
@@ -106,7 +105,7 @@
},
{
"cell_type": "code",
- "execution_count": 14,
+ "execution_count": null,
"metadata": {
"tags": []
},
@@ -137,7 +136,18 @@
},
{
"cell_type": "code",
- "execution_count": 16,
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_scalar(data, key):\n",
+ " v = data[key]\n",
+ " return float(v.iloc[0] if hasattr(v, \"iloc\") else v)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
"metadata": {
"tags": [
"solution"
@@ -188,7 +198,7 @@
" m.fs.state_block.initialize(outlvl=idaeslog.INFO_LOW)\n",
"\n",
" # Fix at actual temperature\n",
- " m.fs.state_block.temperature.fix(float(data[\"temperature\"]))\n",
+ " m.fs.state_block.temperature.fix(get_scalar(data, \"temperature\"))\n",
"\n",
" # Set bounds on variables to be estimated\n",
" m.fs.properties.tau[\"benzene\", \"toluene\"].setlb(-5)\n",
@@ -203,7 +213,7 @@
},
{
"cell_type": "code",
- "execution_count": 17,
+ "execution_count": null,
"metadata": {
"tags": [
"testing"
@@ -273,7 +283,7 @@
},
{
"cell_type": "code",
- "execution_count": 19,
+ "execution_count": null,
"metadata": {
"tags": [
"solution"
@@ -304,7 +314,7 @@
},
{
"cell_type": "code",
- "execution_count": 20,
+ "execution_count": null,
"metadata": {
"tags": []
},
@@ -345,10 +355,10 @@
" # and vapor phase. For example, the squared error for the vapor phase is:\n",
" # (float(data[\"vap_benzene\"]) - m.fs.state_block.mole_frac_phase_comp[\"Vap\", \"benzene\"])**2\n",
" expr = (\n",
- " float(data[\"vap_benzene\"])\n",
+ " get_scalar(data, \"vap_benzene\")\n",
" - m.fs.state_block.mole_frac_phase_comp[\"Vap\", \"benzene\"]\n",
" ) ** 2 + (\n",
- " float(data[\"liq_benzene\"])\n",
+ " get_scalar(data, \"liq_benzene\")\n",
" - m.fs.state_block.mole_frac_phase_comp[\"Liq\", \"benzene\"]\n",
" ) ** 2\n",
" return expr * 1e4"
@@ -465,7 +475,7 @@
"metadata": {
"celltoolbar": "Tags",
"kernelspec": {
- "display_name": "Python 3 (ipykernel)",
+ "display_name": "idaes-examples-dev",
"language": "python",
"name": "python3"
},
@@ -479,9 +489,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.10.9"
+ "version": "3.12.13"
}
},
"nbformat": 4,
"nbformat_minor": 3
-}
\ No newline at end of file
+}
diff --git a/idaes_examples/notebooks/docs/param_est/parameter_estimation_nrtl_using_unit_model_test.ipynb b/idaes_examples/notebooks/docs/param_est/parameter_estimation_nrtl_using_unit_model_test.ipynb
index 7add77ff..96eeba92 100644
--- a/idaes_examples/notebooks/docs/param_est/parameter_estimation_nrtl_using_unit_model_test.ipynb
+++ b/idaes_examples/notebooks/docs/param_est/parameter_estimation_nrtl_using_unit_model_test.ipynb
@@ -1,483 +1,496 @@
{
- "cells": [
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "header",
- "hide-cell"
- ]
- },
- "outputs": [],
- "source": [
- "###############################################################################\n",
- "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n",
- "# Framework (IDAES IP) was produced under the DOE Institute for the\n",
- "# Design of Advanced Energy Systems (IDAES).\n",
- "#\n",
- "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n",
- "# University of California, through Lawrence Berkeley National Laboratory,\n",
- "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n",
- "# University, West Virginia University Research Corporation, et al.\n",
- "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n",
- "# for full copyright and license information.\n",
- "###############################################################################"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Parameter Estimation Using Flash Unit Model\n",
- "\n",
- "Author: Jaffer Ghouse \n",
- "Maintainer: Andrew Lee \n",
- "Updated: 2023-06-01 \n",
- "\n",
- "In this module, we will be using Pyomo's `parmest` tool in conjunction with IDAES models for parameter estimation. We demonstrate these tools by estimating the parameters associated with the NRTL property model for a benzene-toluene mixture. The NRTL model has 2 sets of parameters: the non-randomness parameter (`alpha_ij`) and the binary interaction parameter (`tau_ij`), where `i` and `j` is the pure component species. In this example, we will be only estimate the binary interaction parameter (`tau_ij`) for a given dataset. When estimating parameters associated with the property package, IDAES provides the flexibility of doing the parameter estimation by just using the state block or by using a unit model with a specified property package. This module will demonstrate parameter estimation by using the flash unit model with the NRTL property package. \n",
- "\n",
- "We will complete the following tasks:\n",
- "* Set up a method to return an initialized model\n",
- "* Set up the parameter estimation problem using `parmest`\n",
- "* Analyze the results\n",
- "* Demonstrate advanced features from `parmest`\n",
- "\n",
- "## Key links to documentation:\n",
- "* NRTL Model - https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/property_models/activity_coefficient.html\n",
- "* parmest - https://pyomo.readthedocs.io/en/stable/contributed_packages/parmest/index.html\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "Inline Exercise:\n",
- "import `ConcreteModel` from Pyomo, `FlowsheetBlock` and `Flash` from IDAES. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: import ConcreteModel from pyomo.environ\n",
- "from pyomo.environ import ConcreteModel, value\n",
- "\n",
- "# Todo: import FlowsheetBlock from idaes.core\n",
- "from idaes.core import FlowsheetBlock\n",
- "\n",
- "# Todo: import Flash unit model from idaes.models.unit_models\n",
- "from idaes.models.unit_models import Flash"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "In the next cell, we will be importing the parameter block that we will be using in this module and the idaes logger. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "from idaes.models.properties.activity_coeff_models.BTX_activity_coeff_VLE import (\n",
- " BTXParameterBlock,\n",
- ")\n",
- "import idaes.logger as idaeslog"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "In the next cell, we import `parmest` from Pyomo and the `pandas` package. We need `pandas` as `parmest` uses `pandas.dataframe` for handling the input data and the results."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "import pyomo.contrib.parmest.parmest as parmest\n",
- "import pandas as pd"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Setting up an initialized model\n",
- "\n",
- "We need to provide a method that returns an initialized model to the `parmest` tool in Pyomo."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- "Inline Exercise:\n",
- "Using what you have learned from previous modules, fill in the missing code below to return an initialized IDAES model. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "def NRTL_model(data):\n",
- "\n",
- " # Todo: Create a ConcreteModel object\n",
- " m = ConcreteModel()\n",
- "\n",
- " # Todo: Create FlowsheetBlock object\n",
- " m.fs = FlowsheetBlock(dynamic=False)\n",
- "\n",
- " # Todo: Create a properties parameter object with the following options:\n",
- " # \"valid_phase\": ('Liq', 'Vap')\n",
- " # \"activity_coeff_model\": 'NRTL'\n",
- " m.fs.properties = BTXParameterBlock(\n",
- " valid_phase=(\"Liq\", \"Vap\"), activity_coeff_model=\"NRTL\"\n",
- " )\n",
- " m.fs.flash = Flash(property_package=m.fs.properties)\n",
- "\n",
- " # Initialize at a certain inlet condition\n",
- " m.fs.flash.inlet.flow_mol.fix(1)\n",
- " m.fs.flash.inlet.temperature.fix(368)\n",
- " m.fs.flash.inlet.pressure.fix(101325)\n",
- " m.fs.flash.inlet.mole_frac_comp[0, \"benzene\"].fix(0.5)\n",
- " m.fs.flash.inlet.mole_frac_comp[0, \"toluene\"].fix(0.5)\n",
- "\n",
- " # Set Flash unit specifications\n",
- " m.fs.flash.heat_duty.fix(0)\n",
- " m.fs.flash.deltaP.fix(0)\n",
- "\n",
- " # Fix NRTL specific variables\n",
- " # alpha values (set at 0.3)\n",
- " m.fs.properties.alpha[\"benzene\", \"benzene\"].fix(0)\n",
- " m.fs.properties.alpha[\"benzene\", \"toluene\"].fix(0.3)\n",
- " m.fs.properties.alpha[\"toluene\", \"toluene\"].fix(0)\n",
- " m.fs.properties.alpha[\"toluene\", \"benzene\"].fix(0.3)\n",
- "\n",
- " # initial tau values\n",
- " m.fs.properties.tau[\"benzene\", \"benzene\"].fix(0)\n",
- " m.fs.properties.tau[\"benzene\", \"toluene\"].fix(-0.9)\n",
- " m.fs.properties.tau[\"toluene\", \"toluene\"].fix(0)\n",
- " m.fs.properties.tau[\"toluene\", \"benzene\"].fix(1.4)\n",
- "\n",
- " # Initialize the flash unit\n",
- " m.fs.flash.initialize(outlvl=idaeslog.INFO_LOW)\n",
- "\n",
- " # Fix at actual temperature\n",
- " m.fs.flash.inlet.temperature.fix(float(data[\"temperature\"]))\n",
- "\n",
- " # Set bounds on variables to be estimated\n",
- " m.fs.properties.tau[\"benzene\", \"toluene\"].setlb(-5)\n",
- " m.fs.properties.tau[\"benzene\", \"toluene\"].setub(5)\n",
- "\n",
- " m.fs.properties.tau[\"toluene\", \"benzene\"].setlb(-5)\n",
- " m.fs.properties.tau[\"toluene\", \"benzene\"].setub(5)\n",
- "\n",
- " # Return initialized flash model\n",
- " return m"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "testing"
- ]
- },
- "outputs": [],
- "source": [
- "from idaes.core.util.model_statistics import degrees_of_freedom\n",
- "import pytest\n",
- "\n",
- "# Testing the initialized model\n",
- "test_data = {\"temperature\": 368}\n",
- "\n",
- "m = NRTL_model(test_data)\n",
- "\n",
- "# Check that degrees of freedom is 0\n",
- "assert degrees_of_freedom(m) == 0\n",
- "\n",
- "# Check for output values\n",
- "assert value(m.fs.flash.liq_outlet.mole_frac_comp[0, \"benzene\"]) == pytest.approx(\n",
- " 0.389, abs=1e-2\n",
- ")\n",
- "assert value(m.fs.flash.vap_outlet.mole_frac_comp[0, \"benzene\"]) == pytest.approx(\n",
- " 0.610, abs=1e-2\n",
- ")\n",
- "\n",
- "assert value(m.fs.flash.liq_outlet.mole_frac_comp[0, \"toluene\"]) == pytest.approx(\n",
- " 0.610, abs=1e-2\n",
- ")\n",
- "assert value(m.fs.flash.vap_outlet.mole_frac_comp[0, \"toluene\"]) == pytest.approx(\n",
- " 0.394, abs=1e-2\n",
- ")"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "## Parameter estimation using parmest"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "In addition to providing a method to return an initialized model, the `parmest` tool needs the following:\n",
- "\n",
- "* List of variable names to be estimated\n",
- "* Dataset with multiple scenarios\n",
- "* Expression to compute the sum of squared errors\n",
- "\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "In this example, we only estimate the binary interaction parameter (`tau_ij`). Given that this variable is usually indexed as `tau_ij = Var(component_list, component_list)`, there are 2*2=4 degrees of freedom. However, when i=j, the binary interaction parameter is 0. Therefore, in this problem, we estimate the binary interaction parameter for the following variables only:\n",
- "\n",
- "* fs.properties.tau['benzene', 'toluene']\n",
- "* fs.properties.tau['toluene', 'benzene']\n",
- "\n",
- "\n",
- "Inline Exercise:\n",
- "Create a list called `variable_name` with the above-mentioned variables declared as strings.\n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Todo: Create a list of vars to estimate\n",
- "variable_name = [\n",
- " \"fs.properties.tau['benzene', 'toluene']\",\n",
- " \"fs.properties.tau['toluene', 'benzene']\",\n",
- "]"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Pyomo's `parmest` tool supports the following data formats:\n",
- "- pandas dataframe\n",
- "- list of dictionaries\n",
- "- list of json file names.\n",
- "\n",
- "Please see the documentation for more details. \n",
- "\n",
- "For this example, we load data from the csv file `BT_NRTL_dataset.csv`. The dataset consists of fifty data points which provide the mole fraction of benzene in the vapor and liquid phase as a function of temperature. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Load data from csv\n",
- "data = pd.read_csv(\"BT_NRTL_dataset.csv\")\n",
- "\n",
- "# Display the dataset\n",
- "display(data)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We need to provide a method to return an expression to compute the sum of squared errors that will be used as the objective in solving the parameter estimation problem. For this problem, the error will be computed for the mole fraction of benzene in the vapor and liquid phase between the model prediction and data. \n",
- "\n",
- "\n",
- "Inline Exercise:\n",
- "Complete the following cell by adding an expression to compute the sum of square errors. \n",
- "
"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "solution"
- ]
- },
- "outputs": [],
- "source": [
- "# Create method to return an expression that computes the sum of squared error\n",
- "def SSE(m, data):\n",
- " # Todo: Add expression for computing the sum of squared errors in mole fraction of benzene in the liquid\n",
- " # and vapor phase. For example, the squared error for the vapor phase is:\n",
- " # (float(data[\"vap_benzene\"]) - m.fs.flash.vap_outlet.mole_frac_comp[0, \"benzene\"])**2\n",
- " expr = (\n",
- " float(data[\"vap_benzene\"]) - m.fs.flash.vap_outlet.mole_frac_comp[0, \"benzene\"]\n",
- " ) ** 2 + (\n",
- " float(data[\"liq_benzene\"]) - m.fs.flash.liq_outlet.mole_frac_comp[0, \"benzene\"]\n",
- " ) ** 2\n",
- " return expr * 1e4"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "\n",
- "Note:\n",
- "Notice that we have scaled the expression up by a factor of 10000 as the SSE computed here will be an extremely small number given that we are using the difference in mole fraction in our expression. A well-scaled objective will help improve solve robustness when using IPOPT. \n",
- "
\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "We are now ready to set up the parameter estimation problem. We will create a parameter estimation object called `pest`. As shown below, we pass the method that returns an initialized model, dataset, list of variable names to estimate, and the SSE expression to the Estimator object. `tee=True` will print the solver output after solving the parameter estimation problem."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Initialize a parameter estimation object\n",
- "pest = parmest.Estimator(NRTL_model, data, variable_name, SSE, tee=True)\n",
- "\n",
- "# Run parameter estimation using all data\n",
- "obj_value, parameters = pest.theta_est()"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {
- "tags": [
- "testing"
- ]
- },
- "outputs": [],
- "source": [
- "# Check for values of the parameter estimation problem\n",
- "assert obj_value == pytest.approx(5.07496, abs=1e-1)\n",
- "assert parameters[\"fs.properties.tau[benzene,toluene]\"] == pytest.approx(-0.89876, 1e-3)\n",
- "assert parameters[\"fs.properties.tau[toluene,benzene]\"] == pytest.approx(1.410486, 1e-3)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "You will notice that the resulting parameter estimation problem, when using the flash unit model, will have 2952 variables and 2950 constraints. This is because the unit models in IDAES use control volume blocks which have two state blocks attached; one at the inlet and one at the outlet. Even though there are two state blocks, they still use the same parameter block i.e. `m.fs.properties` in our example which is where our parameters that need to be estimated exist. \n",
- "\n",
- "Let us display the results by running the next cell. "
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "print(\"The SSE at the optimal solution is %0.6f\" % (obj_value * 1e-4))\n",
- "print()\n",
- "print(\"The values for the parameters are as follows:\")\n",
- "for k, v in parameters.items():\n",
- " print(k, \"=\", v)"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Using the data that was provided, we have estimated the binary interaction parameters in the NRTL model for a benzene-toluene mixture. Although the dataset that was provided was temperature dependent, in this example we have estimated a single value that fits best for all temperatures."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "### Advanced options for parmest: bootstrapping\n",
- "\n"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Pyomo's `parmest` tool allows for bootstrapping where the parameter estimation is repeated over `n` samples with resampling from the original data set. Parameter estimation with bootstrap resampling can be used to identify confidence regions around each parameter estimate. This analysis can be slow given the increased number of model instances that need to be solved. Please refer to https://pyomo.readthedocs.io/en/stable/contributed_packages/parmest/driver.html for more details. \n",
- "\n",
- "For the example above, the bootstrapping can be run by uncommenting the code in the following cell:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": [
- "# Run parameter estimation using bootstrap resample of the data (10 samples),\n",
- "# plot results along with confidence regions\n",
- "\n",
- "# Uncomment the following lines\n",
- "\n",
- "# bootstrap_theta = pest.theta_est_bootstrap(4)\n",
- "# display(bootstrap_theta)"
- ]
- }
- ],
- "metadata": {
- "celltoolbar": "Tags",
- "kernelspec": {
- "display_name": "Python 3 (ipykernel)",
- "language": "python",
- "name": "python3"
- },
- "language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
- "name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.9.12"
- }
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "tags": [
+ "header",
+ "hide-cell"
+ ]
+ },
+ "outputs": [],
+ "source": [
+ "###############################################################################\n",
+ "# The Institute for the Design of Advanced Energy Systems Integrated Platform\n",
+ "# Framework (IDAES IP) was produced under the DOE Institute for the\n",
+ "# Design of Advanced Energy Systems (IDAES).\n",
+ "#\n",
+ "# Copyright (c) 2018-2023 by the software owners: The Regents of the\n",
+ "# University of California, through Lawrence Berkeley National Laboratory,\n",
+ "# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon\n",
+ "# University, West Virginia University Research Corporation, et al.\n",
+ "# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md\n",
+ "# for full copyright and license information.\n",
+ "###############################################################################"
+ ]
},
- "nbformat": 4,
- "nbformat_minor": 3
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Parameter Estimation Using Flash Unit Model\n",
+ "\n",
+ "Author: Jaffer Ghouse \n",
+ "Maintainer: Andrew Lee \n",
+ "Updated: 2023-06-01 \n",
+ "\n",
+ "In this module, we will be using Pyomo's `parmest` tool in conjunction with IDAES models for parameter estimation. We demonstrate these tools by estimating the parameters associated with the NRTL property model for a benzene-toluene mixture. The NRTL model has 2 sets of parameters: the non-randomness parameter (`alpha_ij`) and the binary interaction parameter (`tau_ij`), where `i` and `j` is the pure component species. In this example, we will be only estimate the binary interaction parameter (`tau_ij`) for a given dataset. When estimating parameters associated with the property package, IDAES provides the flexibility of doing the parameter estimation by just using the state block or by using a unit model with a specified property package. This module will demonstrate parameter estimation by using the flash unit model with the NRTL property package. \n",
+ "\n",
+ "We will complete the following tasks:\n",
+ "* Set up a method to return an initialized model\n",
+ "* Set up the parameter estimation problem using `parmest`\n",
+ "* Analyze the results\n",
+ "* Demonstrate advanced features from `parmest`\n",
+ "\n",
+ "## Key links to documentation:\n",
+ "* NRTL Model - https://idaes-pse.readthedocs.io/en/stable/reference_guides/model_libraries/generic/property_models/activity_coefficient.html\n",
+ "* parmest - https://pyomo.readthedocs.io/en/stable/contributed_packages/parmest/index.html\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "Inline Exercise:\n",
+ "import `ConcreteModel` from Pyomo, `FlowsheetBlock` and `Flash` from IDAES. \n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "tags": [
+ "solution"
+ ]
+ },
+ "outputs": [],
+ "source": [
+ "# Todo: import ConcreteModel from pyomo.environ\n",
+ "from pyomo.environ import ConcreteModel, value\n",
+ "\n",
+ "# Todo: import FlowsheetBlock from idaes.core\n",
+ "from idaes.core import FlowsheetBlock\n",
+ "\n",
+ "# Todo: import Flash unit model from idaes.models.unit_models\n",
+ "from idaes.models.unit_models import Flash"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In the next cell, we will be importing the parameter block that we will be using in this module and the idaes logger. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from idaes.models.properties.activity_coeff_models.BTX_activity_coeff_VLE import (\n",
+ " BTXParameterBlock,\n",
+ ")\n",
+ "import idaes.logger as idaeslog"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In the next cell, we import `parmest` from Pyomo and the `pandas` package. We need `pandas` as `parmest` uses `pandas.dataframe` for handling the input data and the results."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import pyomo.contrib.parmest.parmest as parmest\n",
+ "import pandas as pd"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Setting up an initialized model\n",
+ "\n",
+ "We need to provide a method that returns an initialized model to the `parmest` tool in Pyomo."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "Inline Exercise:\n",
+ "Using what you have learned from previous modules, fill in the missing code below to return an initialized IDAES model. \n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_scalar(data, key):\n",
+ " v = data[key]\n",
+ " return float(v.iloc[0] if hasattr(v, \"iloc\") else v)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "tags": [
+ "solution"
+ ]
+ },
+ "outputs": [],
+ "source": [
+ "def NRTL_model(data):\n",
+ "\n",
+ " # Todo: Create a ConcreteModel object\n",
+ " m = ConcreteModel()\n",
+ "\n",
+ " # Todo: Create FlowsheetBlock object\n",
+ " m.fs = FlowsheetBlock(dynamic=False)\n",
+ "\n",
+ " # Todo: Create a properties parameter object with the following options:\n",
+ " # \"valid_phase\": ('Liq', 'Vap')\n",
+ " # \"activity_coeff_model\": 'NRTL'\n",
+ " m.fs.properties = BTXParameterBlock(\n",
+ " valid_phase=(\"Liq\", \"Vap\"), activity_coeff_model=\"NRTL\"\n",
+ " )\n",
+ " m.fs.flash = Flash(property_package=m.fs.properties)\n",
+ "\n",
+ " # Initialize at a certain inlet condition\n",
+ " m.fs.flash.inlet.flow_mol.fix(1)\n",
+ " m.fs.flash.inlet.temperature.fix(368)\n",
+ " m.fs.flash.inlet.pressure.fix(101325)\n",
+ " m.fs.flash.inlet.mole_frac_comp[0, \"benzene\"].fix(0.5)\n",
+ " m.fs.flash.inlet.mole_frac_comp[0, \"toluene\"].fix(0.5)\n",
+ "\n",
+ " # Set Flash unit specifications\n",
+ " m.fs.flash.heat_duty.fix(0)\n",
+ " m.fs.flash.deltaP.fix(0)\n",
+ "\n",
+ " # Fix NRTL specific variables\n",
+ " # alpha values (set at 0.3)\n",
+ " m.fs.properties.alpha[\"benzene\", \"benzene\"].fix(0)\n",
+ " m.fs.properties.alpha[\"benzene\", \"toluene\"].fix(0.3)\n",
+ " m.fs.properties.alpha[\"toluene\", \"toluene\"].fix(0)\n",
+ " m.fs.properties.alpha[\"toluene\", \"benzene\"].fix(0.3)\n",
+ "\n",
+ " # initial tau values\n",
+ " m.fs.properties.tau[\"benzene\", \"benzene\"].fix(0)\n",
+ " m.fs.properties.tau[\"benzene\", \"toluene\"].fix(-0.9)\n",
+ " m.fs.properties.tau[\"toluene\", \"toluene\"].fix(0)\n",
+ " m.fs.properties.tau[\"toluene\", \"benzene\"].fix(1.4)\n",
+ "\n",
+ " # Initialize the flash unit\n",
+ " m.fs.flash.initialize(outlvl=idaeslog.INFO_LOW)\n",
+ "\n",
+ " # Fix at actual temperature\n",
+ " m.fs.flash.inlet.temperature.fix(get_scalar(data, \"temperature\"))\n",
+ "\n",
+ " # Set bounds on variables to be estimated\n",
+ " m.fs.properties.tau[\"benzene\", \"toluene\"].setlb(-5)\n",
+ " m.fs.properties.tau[\"benzene\", \"toluene\"].setub(5)\n",
+ "\n",
+ " m.fs.properties.tau[\"toluene\", \"benzene\"].setlb(-5)\n",
+ " m.fs.properties.tau[\"toluene\", \"benzene\"].setub(5)\n",
+ "\n",
+ " # Return initialized flash model\n",
+ " return m"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "tags": [
+ "testing"
+ ]
+ },
+ "outputs": [],
+ "source": [
+ "from idaes.core.util.model_statistics import degrees_of_freedom\n",
+ "import pytest\n",
+ "\n",
+ "# Testing the initialized model\n",
+ "test_data = {\"temperature\": 368}\n",
+ "\n",
+ "m = NRTL_model(test_data)\n",
+ "\n",
+ "# Check that degrees of freedom is 0\n",
+ "assert degrees_of_freedom(m) == 0\n",
+ "\n",
+ "# Check for output values\n",
+ "assert value(m.fs.flash.liq_outlet.mole_frac_comp[0, \"benzene\"]) == pytest.approx(\n",
+ " 0.389, abs=1e-2\n",
+ ")\n",
+ "assert value(m.fs.flash.vap_outlet.mole_frac_comp[0, \"benzene\"]) == pytest.approx(\n",
+ " 0.610, abs=1e-2\n",
+ ")\n",
+ "\n",
+ "assert value(m.fs.flash.liq_outlet.mole_frac_comp[0, \"toluene\"]) == pytest.approx(\n",
+ " 0.610, abs=1e-2\n",
+ ")\n",
+ "assert value(m.fs.flash.vap_outlet.mole_frac_comp[0, \"toluene\"]) == pytest.approx(\n",
+ " 0.394, abs=1e-2\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Parameter estimation using parmest"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In addition to providing a method to return an initialized model, the `parmest` tool needs the following:\n",
+ "\n",
+ "* List of variable names to be estimated\n",
+ "* Dataset with multiple scenarios\n",
+ "* Expression to compute the sum of squared errors\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In this example, we only estimate the binary interaction parameter (`tau_ij`). Given that this variable is usually indexed as `tau_ij = Var(component_list, component_list)`, there are 2*2=4 degrees of freedom. However, when i=j, the binary interaction parameter is 0. Therefore, in this problem, we estimate the binary interaction parameter for the following variables only:\n",
+ "\n",
+ "* fs.properties.tau['benzene', 'toluene']\n",
+ "* fs.properties.tau['toluene', 'benzene']\n",
+ "\n",
+ "\n",
+ "Inline Exercise:\n",
+ "Create a list called `variable_name` with the above-mentioned variables declared as strings.\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "tags": [
+ "solution"
+ ]
+ },
+ "outputs": [],
+ "source": [
+ "# Todo: Create a list of vars to estimate\n",
+ "variable_name = [\n",
+ " \"fs.properties.tau['benzene', 'toluene']\",\n",
+ " \"fs.properties.tau['toluene', 'benzene']\",\n",
+ "]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Pyomo's `parmest` tool supports the following data formats:\n",
+ "- pandas dataframe\n",
+ "- list of dictionaries\n",
+ "- list of json file names.\n",
+ "\n",
+ "Please see the documentation for more details. \n",
+ "\n",
+ "For this example, we load data from the csv file `BT_NRTL_dataset.csv`. The dataset consists of fifty data points which provide the mole fraction of benzene in the vapor and liquid phase as a function of temperature. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Load data from csv\n",
+ "data = pd.read_csv(\"BT_NRTL_dataset.csv\")\n",
+ "\n",
+ "# Display the dataset\n",
+ "display(data)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We need to provide a method to return an expression to compute the sum of squared errors that will be used as the objective in solving the parameter estimation problem. For this problem, the error will be computed for the mole fraction of benzene in the vapor and liquid phase between the model prediction and data. \n",
+ "\n",
+ "\n",
+ "Inline Exercise:\n",
+ "Complete the following cell by adding an expression to compute the sum of square errors. \n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "tags": [
+ "solution"
+ ]
+ },
+ "outputs": [],
+ "source": [
+ "# Create method to return an expression that computes the sum of squared error\n",
+ "def SSE(m, data):\n",
+ " # Todo: Add expression for computing the sum of squared errors in mole fraction of benzene in the liquid\n",
+ " # and vapor phase. For example, the squared error for the vapor phase is:\n",
+ " # (float(data[\"vap_benzene\"]) - m.fs.flash.vap_outlet.mole_frac_comp[0, \"benzene\"])**2\n",
+ " expr = (\n",
+ " get_scalar(data, \"vap_benzene\")\n",
+ " - m.fs.flash.vap_outlet.mole_frac_comp[0, \"benzene\"]\n",
+ " ) ** 2 + (\n",
+ " get_scalar(data, \"liq_benzene\")\n",
+ " - m.fs.flash.liq_outlet.mole_frac_comp[0, \"benzene\"]\n",
+ " ) ** 2\n",
+ " return expr * 1e4"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "\n",
+ "Note:\n",
+ "Notice that we have scaled the expression up by a factor of 10000 as the SSE computed here will be an extremely small number given that we are using the difference in mole fraction in our expression. A well-scaled objective will help improve solve robustness when using IPOPT. \n",
+ "
\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We are now ready to set up the parameter estimation problem. We will create a parameter estimation object called `pest`. As shown below, we pass the method that returns an initialized model, dataset, list of variable names to estimate, and the SSE expression to the Estimator object. `tee=True` will print the solver output after solving the parameter estimation problem."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Initialize a parameter estimation object\n",
+ "pest = parmest.Estimator(NRTL_model, data, variable_name, SSE, tee=True)\n",
+ "\n",
+ "# Run parameter estimation using all data\n",
+ "obj_value, parameters = pest.theta_est()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "tags": [
+ "testing"
+ ]
+ },
+ "outputs": [],
+ "source": [
+ "# Check for values of the parameter estimation problem\n",
+ "assert obj_value == pytest.approx(5.07496, abs=1e-1)\n",
+ "assert parameters[\"fs.properties.tau[benzene,toluene]\"] == pytest.approx(-0.89876, 1e-3)\n",
+ "assert parameters[\"fs.properties.tau[toluene,benzene]\"] == pytest.approx(1.410486, 1e-3)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "You will notice that the resulting parameter estimation problem, when using the flash unit model, will have 2952 variables and 2950 constraints. This is because the unit models in IDAES use control volume blocks which have two state blocks attached; one at the inlet and one at the outlet. Even though there are two state blocks, they still use the same parameter block i.e. `m.fs.properties` in our example which is where our parameters that need to be estimated exist. \n",
+ "\n",
+ "Let us display the results by running the next cell. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(\"The SSE at the optimal solution is %0.6f\" % (obj_value * 1e-4))\n",
+ "print()\n",
+ "print(\"The values for the parameters are as follows:\")\n",
+ "for k, v in parameters.items():\n",
+ " print(k, \"=\", v)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Using the data that was provided, we have estimated the binary interaction parameters in the NRTL model for a benzene-toluene mixture. Although the dataset that was provided was temperature dependent, in this example we have estimated a single value that fits best for all temperatures."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "### Advanced options for parmest: bootstrapping\n",
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Pyomo's `parmest` tool allows for bootstrapping where the parameter estimation is repeated over `n` samples with resampling from the original data set. Parameter estimation with bootstrap resampling can be used to identify confidence regions around each parameter estimate. This analysis can be slow given the increased number of model instances that need to be solved. Please refer to https://pyomo.readthedocs.io/en/stable/contributed_packages/parmest/driver.html for more details. \n",
+ "\n",
+ "For the example above, the bootstrapping can be run by uncommenting the code in the following cell:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Run parameter estimation using bootstrap resample of the data (10 samples),\n",
+ "# plot results along with confidence regions\n",
+ "\n",
+ "# Uncomment the following lines\n",
+ "\n",
+ "# bootstrap_theta = pest.theta_est_bootstrap(4)\n",
+ "# display(bootstrap_theta)"
+ ]
+ }
+ ],
+ "metadata": {
+ "celltoolbar": "Tags",
+ "kernelspec": {
+ "display_name": "idaes-examples-dev",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.13"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 3
}
diff --git a/idaes_examples/notebooks/docs/properties/parameter_estimation_pr_test.ipynb b/idaes_examples/notebooks/docs/properties/parameter_estimation_pr_test.ipynb
index 4d620a5a..20644a1b 100644
--- a/idaes_examples/notebooks/docs/properties/parameter_estimation_pr_test.ipynb
+++ b/idaes_examples/notebooks/docs/properties/parameter_estimation_pr_test.ipynb
@@ -57,7 +57,7 @@
},
{
"cell_type": "code",
- "execution_count": 1,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -83,7 +83,7 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -99,7 +99,7 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -121,7 +121,7 @@
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -146,12 +146,24 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
- "def PR_model(data):\n",
- "\n",
+ "def get_scalar(data, key):\n",
+ " v = data[key]\n",
+ " return float(v.iloc[0] if hasattr(v, \"iloc\") else v)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def PR_model(experiment_number=None, cb_data=None):\n",
+ " # cb_data is typically the row/record for this experiment\n",
+ " data = cb_data if cb_data is not None else experiment_number\n",
" m = ConcreteModel()\n",
"\n",
" m.fs = FlowsheetBlock(dynamic=False)\n",
@@ -161,9 +173,9 @@
" m.fs.state_block = m.fs.properties.build_state_block([1], defined_state=True)\n",
"\n",
" m.fs.state_block[1].flow_mol.fix(1)\n",
- " x = float(data[\"x_carbon_dioxide\"]) + 0.5\n",
- " m.fs.state_block[1].temperature.fix(float(data[\"temperature\"]))\n",
- " m.fs.state_block[1].pressure.fix(float(data[\"pressure\"]))\n",
+ " x = get_scalar(data, \"x_carbon_dioxide\") + 0.5\n",
+ " m.fs.state_block[1].temperature.fix(get_scalar(data, \"temperature\"))\n",
+ " m.fs.state_block[1].pressure.fix(get_scalar(data, \"pressure\"))\n",
" m.fs.state_block[1].mole_frac_comp[\"bmimPF6\"].fix(1 - x)\n",
" m.fs.state_block[1].mole_frac_comp[\"carbon_dioxide\"].fix(x)\n",
"\n",
@@ -178,14 +190,14 @@
"\n",
" # Fix the state variables on the state block\n",
" m.fs.state_block[1].pressure.unfix()\n",
- " m.fs.state_block[1].temperature.fix(float(data[\"temperature\"]))\n",
+ " m.fs.state_block[1].temperature.fix(get_scalar(data, \"temperature\"))\n",
" m.fs.state_block[1].mole_frac_phase_comp[\"Liq\", \"bmimPF6\"].fix(\n",
- " float(data[\"x_bmimPF6\"])\n",
+ " get_scalar(data, \"x_bmimPF6\")\n",
" )\n",
" m.fs.state_block[1].mole_frac_phase_comp[\"Liq\", \"carbon_dioxide\"].fix(\n",
- " float(data[\"x_carbon_dioxide\"])\n",
+ " get_scalar(data, \"x_carbon_dioxide\")\n",
" )\n",
- " m.fs.state_block[1].mole_frac_comp[\"bmimPF6\"].fix(float(data[\"x_bmimPF6\"]))\n",
+ " m.fs.state_block[1].mole_frac_comp[\"bmimPF6\"].fix(get_scalar(data, \"x_bmimPF6\"))\n",
" m.fs.state_block[1].mole_frac_comp[\"carbon_dioxide\"].unfix()\n",
" # Set bounds on variables to be estimated\n",
" m.fs.properties.PR_kappa[\"bmimPF6\", \"carbon_dioxide\"].setlb(-5)\n",
@@ -207,7 +219,7 @@
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": null,
"metadata": {
"scrolled": false
},
@@ -242,7 +254,7 @@
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -263,12 +275,12 @@
},
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def SSE(m, data):\n",
- " expr = (float(data[\"pressure\"]) - m.fs.state_block[1].pressure) ** 2\n",
+ " expr = (get_scalar(data, \"pressure\") - m.fs.state_block[1].pressure) ** 2\n",
" return expr * 1e-7"
]
},
@@ -283,7 +295,7 @@
},
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": null,
"metadata": {
"scrolled": false
},
@@ -305,7 +317,7 @@
},
{
"cell_type": "code",
- "execution_count": 10,
+ "execution_count": null,
"metadata": {},
"outputs": [],
"source": [
@@ -322,18 +334,11 @@
"source": [
"Now we can use this parameters and include them in the configuration dictionary. We can also use `m.fs.properties = GenericParameterBlock(**configuration)` to solve unit models."
]
- },
- {
- "cell_type": "code",
- "execution_count": null,
- "metadata": {},
- "outputs": [],
- "source": []
}
],
"metadata": {
"kernelspec": {
- "display_name": "Python 3 (ipykernel)",
+ "display_name": "idaes-examples-dev",
"language": "python",
"name": "python3"
},
@@ -347,7 +352,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.12"
+ "version": "3.12.13"
}
},
"nbformat": 4,
diff --git a/pyproject.toml b/pyproject.toml
index 4f8c0357..de6ab766 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,7 +54,7 @@ omlt = [
"tensorflow >= 2.16.1",
]
idaes = [
- "idaes-pse", # installing IDAES (from release) is opt-in
+ "idaes-pse @ git+https://github.com/sufikaur/idaes-pse@issue-1802", # installing IDAES (from release) is opt-in
]
testing = [
# parallel pytest
diff --git a/requirements-dev.txt b/requirements-dev.txt
index f8255530..aa569e0f 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,6 +1,6 @@
--editable .[dev,omlt]
-idaes-pse @ git+https://github.com/IDAES/idaes-pse@main
+idaes-pse @ git+https://github.com/sufikaur/idaes-pse@issue-1802
# 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
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..4d3a4069
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,242 @@
+absl-py==2.3.1
+accessible-pygments==0.0.5
+addheader==0.3.2
+alabaster==0.7.16
+annotated-types==0.7.0
+anyio==4.11.0
+appnope==0.1.4
+argon2-cffi==25.1.0
+argon2-cffi-bindings==25.1.0
+arrow==1.3.0
+astroid==2.15.8
+asttokens==3.0.0
+astunparse==1.6.3
+async-lru==2.0.5
+attrs==25.4.0
+babel==2.17.0
+beautifulsoup4==4.14.2
+black==24.3.0
+bleach==6.2.0
+blessed==1.20.0
+certifi==2025.10.5
+cffi==2.0.0
+cfgv==3.4.0
+charset-normalizer==3.4.4
+click==8.3.0
+comm==0.2.3
+contourpy==1.3.3
+coolprop==7.1.0
+coramin==0.1.1
+coverage==7.10.7
+cryptography==46.0.2
+cycler==0.12.1
+debugpy==1.8.17
+decorator==5.2.1
+defusedxml==0.7.1
+dill==0.4.0
+distlib==0.4.0
+docutils==0.18.1
+execnet==2.1.1
+executing==2.2.1
+fastjsonschema==2.21.2
+filelock==3.20.0
+flatbuffers==25.9.23
+flexcache==0.3
+flexparser==0.4
+fonttools==4.60.1
+fqdn==1.5.1
+gast==0.6.0
+google-pasta==0.2.0
+greenlet==3.2.4
+gridx-egret==0.5.5
+gridx-prescient==2.2.2
+grpcio==1.75.1
+h11==0.16.0
+h5py==3.15.0
+httpcore==1.0.9
+httpx==0.28.1
+idaes-compatibility @ git+https://github.com/IDAES/idaes-compatibility@73fdd9e4753470e62d1e84f8b27ae79bec2784e7
+-e git+ssh://git@github.com/sufikaur/examples.git@0b4c00b8d915845406f1836980b69ab0989e5682#egg=idaes_examples
+idaes-flowsheet-processor @ git+https://github.com/prommis/idaes-flowsheet-processor.git@5d282e1b2cae7f86b0c7f2baf9320fb7d607e865
+idaes-pse @ git+https://github.com/IDAES/idaes-pse@ce7f3bd8eeb0883aad9d8d38e98f8a431c643e4a
+idaes-ui==0.25.10
+identify==2.6.15
+idna==3.11
+imagesize==1.4.1
+importlib_metadata==8.7.0
+importlib_resources==6.5.2
+iniconfig==2.1.0
+ipykernel==7.0.1
+ipython==9.6.0
+ipython_pygments_lexers==1.1.1
+ipywidgets==8.1.7
+isoduration==20.11.0
+isort==5.13.2
+jedi==0.19.2
+Jinja2==3.1.6
+json5==0.12.1
+jsonpointer==3.0.0
+jsonschema==4.25.1
+jsonschema-specifications==2025.9.1
+jupyter==1.1.1
+jupyter-book==0.15.1
+jupyter-cache==0.6.1
+jupyter-console==6.6.3
+jupyter-events==0.12.0
+jupyter-lsp==2.3.0
+jupyter_client==8.6.3
+jupyter_core==5.8.1
+jupyter_server==2.17.0
+jupyter_server_terminals==0.5.3
+jupyterlab==4.4.9
+jupyterlab_pygments==0.3.0
+jupyterlab_server==2.27.3
+jupyterlab_widgets==3.0.15
+keras==3.11.3
+kiwisolver==1.4.9
+lark==1.3.0
+latexcodec==3.0.1
+lazy-object-proxy==1.12.0
+libclang==18.1.1
+linkify-it-py==2.0.3
+Markdown==3.9
+markdown-it-py==2.2.0
+MarkupSafe==3.0.3
+matplotlib==3.10.7
+matplotlib-inline==0.1.7
+mccabe==0.7.0
+mdit-py-plugins==0.3.5
+mdurl==0.1.2
+mistune==3.1.4
+ml_dtypes==0.5.3
+mpmath==1.3.0
+mypy_extensions==1.1.0
+myst-nb==0.17.2
+myst-parser==0.18.1
+namex==0.1.0
+narwhals==2.8.0
+nbclient==0.6.8
+nbconvert==7.16.6
+nbformat==5.10.4
+nbmake==1.5.5
+nbsphinx==0.9.7
+nest-asyncio==1.6.0
+networkx==3.5
+nodeenv==1.9.1
+notebook==7.4.7
+notebook_shim==0.2.4
+numpy==2.3.3
+omlt==1.1
+onnx==1.19.1
+opt_einsum==3.4.0
+optree==0.17.0
+overrides==7.7.0
+packaging==25.0
+pandas==2.3.3
+pandocfilters==1.5.1
+parameter-sweep==0.1.0
+parso==0.8.5
+pathspec==0.12.1
+pexpect==4.9.0
+pillow==11.3.0
+Pint==0.24.4
+platformdirs==4.5.0
+playwright==1.55.0
+plotly==6.3.1
+pluggy==1.6.0
+ply==3.11
+pockets==0.9.1
+pre_commit==4.3.0
+prometheus_client==0.23.1
+prompt_toolkit==3.0.52
+protobuf==6.32.1
+psutil==7.1.0
+ptyprocess==0.7.0
+pure_eval==0.2.3
+pybtex==0.25.1
+pybtex-docutils==1.0.3
+pycparser==2.23
+pydantic==2.12.2
+pydantic_core==2.41.4
+pydata-sphinx-theme==0.15.4
+pyee==13.0.0
+Pygments==2.19.2
+pylint==2.17.7
+pyomo==6.9.4
+pyparsing==3.2.5
+pytest==7.4.4
+pytest-cov==7.0.0
+pytest-reportlog==0.1.2
+pytest-xdist==3.0.2
+python-dateutil==2.9.0.post0
+python-json-logger==4.0.0
+pytz==2025.2
+PyYAML==6.0.3
+pyzmq==27.1.0
+referencing==0.37.0
+requests==2.32.5
+rfc3339-validator==0.1.4
+rfc3986-validator==0.1.1
+rfc3987-syntax==1.1.0
+rich==14.2.0
+roman-numerals-py==3.1.0
+rpds-py==0.27.1
+scipy==1.16.2
+seaborn==0.13.2
+Send2Trash==1.8.3
+six==1.17.0
+sniffio==1.3.1
+snowballstemmer==3.0.1
+soupsieve==2.8
+Sphinx==5.0.2
+sphinx-argparse==0.4.0
+sphinx-book-theme==1.0.1
+sphinx-comments==0.0.3
+sphinx-copybutton==0.5.2
+sphinx-jupyterbook-latex==0.5.2
+sphinx-multitoc-numbering==0.1.3
+sphinx-rtd-theme==3.0.2
+sphinx-thebe==0.2.1
+sphinx-togglebutton==0.3.2
+sphinx_design==0.3.0
+sphinx_external_toc==0.3.1
+sphinxcontrib-applehelp==2.0.0
+sphinxcontrib-bibtex==2.5.0
+sphinxcontrib-devhelp==2.0.0
+sphinxcontrib-htmlhelp==2.1.0
+sphinxcontrib-jquery==4.1
+sphinxcontrib-jsmath==1.0.1
+sphinxcontrib-napoleon==0.7
+sphinxcontrib-qthelp==2.0.0
+sphinxcontrib-serializinghtml==2.0.0
+SQLAlchemy==2.0.44
+stack-data==0.6.3
+sympy==1.14.0
+tabulate==0.9.0
+tensorboard==2.20.0
+tensorboard-data-server==0.7.2
+tensorflow==2.20.0
+termcolor==3.1.0
+terminado==0.18.1
+tinycss2==1.4.0
+tokenize_rt==6.2.0
+tomlkit==0.13.3
+tornado==6.5.2
+traitlets==5.14.3
+types-python-dateutil==2.9.0.20251008
+typing-inspection==0.4.2
+typing_extensions==4.15.0
+tzdata==2025.2
+uc-micro-py==1.0.3
+uri-template==1.3.0
+urllib3==2.5.0
+virtualenv==20.35.3
+watertap-solvers==24.12.9
+wcwidth==0.2.14
+webcolors==24.11.1
+webencodings==0.5.1
+websocket-client==1.9.0
+Werkzeug==3.1.3
+widgetsnbextension==4.0.14
+wrapt==1.17.3
+zipp==3.23.0