Tutorial 6 – Creating New Unit Models¶
Introduction¶
The previous tutorial demonstrated how we can modify existing unit models, but we will often run into situations where there is no existing model that is suitable for our needs. In these cases, we will need to build a model for our unit from the ground up.
The IDAES modeling framework has been built to provide a large degree of flexibility to the user when developing new models, whilst providing tools to facilitate many of the common tasks associated with developing new models. This tutorial will guide you through the development of a new unit model using the IDAES CSTR unit model as an example. The actual IDAES CSTR unit model can be found at idaes/core/models/cstr.py for those who wish to see the final code.
This tutorial will teach you about:
- IDAES modeling standards,
- IDAES core modeling classes,
- Creating a new unit model,
- Config blocks and how they are used,
- Holdup blocks and how to interact with them,
- the build_inlets and build_outlets methods, and,
- writing initialization methods.
IDAES Modeling Standards¶
The IDAES modeling framework relies on Flowsheets, Unit Models and Property Packages being able to communicate to each other with a minimum of effort on the part of the user. In order to do this, IDAES has developed a set of modeling standards which all models should conform to. For developers of unit models, the most important of these to be aware of is the IDAES standard naming convention for thermophysical, transport and reaction properties. The standard names for all properties are listed in the IDAES standards documentation.
Components of Unit Models¶
Whilst every unit model is different, there are many common features between them. Every unit model will have some number of inlets and outlets, and at least one set of balance equations. The IDAES modeling framework endeavors to facilitate the construction of these common features by providing a set of core classes and methods to automate their construction.
All unit models in IDAES start by inheriting from the IDAES UnitBlockData class. This class contains a set of methods for automating things like setting up the time domain and creating the inlets and outlets to the unit. Users can find documentation on the UnitBlockData class in the Unit Model documentation.
The next class used in developing unit models within IDAES are the Holdup block classes. These classes are used to automatically generate the material, energy and momentum balances for a specified volume of material (control volume) within the unit. The user is able to provide a simple set of instructions on what terms they wish to include in a given Holdup, and a unit model may contain as many Holdup blocks as required. The IDAES modeling framework currently supports three different types of Holdup blocks, and details of each can be found in the IDAES documentation for Holdup Blocks.
Some additional classes that are indirectly with the development of unit models, which you will not need to use directly but should be aware of are:
As we have seen in earlier tutorials, property packages are used to calculate the various properties required for solving process models. Within the IDAES framework, each Holdup block is associated with a number of Property Blocks, each of which represents a single material state within the unit.
Inlet Mixers and Outlet Splitters are used to support multiple streams connecting to a single inlet or outlet in a unit model. These are used within IDAES for developing general mixer, splitter and separator models, as well as for developing superstructure based process synthesis problems.
The general structure of an IDAES unit model is shown in the figure below.

Creating a New Unit Model¶
As with any model, the first steps in developing a new unit model are to import the necessary components from the Pyomo and IDAES libraries. For this tutorial, the first thing we need to import is the Var object from pyomo.environ.
from pyomo.environ import Var
The next thing we are going to need is ConfigValue from pyomo.common.config. pyomo.common is a library of useful tool provided as part of Pyomo, and the config sub-library contains a number of tools that facilitate passing and validation of construction arguments to Pyomo models.
from pyomo.common.config import ConfigValue
From the IDAES core libraries, we are going to need to import UnitBlockData, declare_process_block_class, Holdup0D and CONFIG_Base.
from idaes.core import UnitBlockData, declare_process_block_class, Holdup0D, CONFIG_Base
Additionally, we will also make use of the following utility functions provided by IDAES.
from idaes.core.util.config import is_parameter_block, list_of_strings
from idaes.core.util.misc import add_object_ref
Creating the Unit Model Class¶
The next step is to create the class which will be used to construct our new unit model. Similar to the previous tutorial, we need to declare a new class (which will form our BlockData class) and to apply the declare_process_block_class decorator to it. In this case, our class will inherit from UnitBlockData, which is the IDAES core class which forms the basis of all unit models in the core library.
@declare_process_block_class("CSTR")
class CSTRData(UnitBlockData):
Config Blocks¶
As we have seen in the previous tutorials, many models within IDAES (in fact all models) have optional construction arguments that the user can provide to control what type of model is constructed. This is automatically handled within IDAES by using config blocks, which are a Pyomo feature designed specifically for handling and organizing construction arguments for models (Note: despite the name, config blocks are not Blocks in the sense of the Pyomo component). For those with understanding of Python dict objects, config blocks can be thought of as specialized dicts with added the added features of validation and documentation of each key-value pair.
Each model class must define its own config block, along with the different construction arguments that it contains and their default values, domains (valid values) and documentation. To save model developers from having to write all of these themselves, IDAES provided a default config block which is suitable for many common unit models. In order to make use of this config block, all we need to do is make a local instance of the config block at the class level of our new class (i.e. immediately after the class declaration and not inside any method definition) as shown below:
CONFIG = CONFIG_Base()
The default IDAES config block defines a number of construction arguments which are common to most unit models, and further documentation on these can be found in the documentation for Holdup blocks. The arguments in the default config block are used by any associated Holdup blocks to determine which terms should be included in the material, energy and momentum balances when they are constructed.
Each of the construction arguments present in the default config block indicates whether a specific phenomenon is expected to occur within a unit, or what form of balance equation needs to be written for the unit in question. The default config block assigns a default value to each of these, however each unit model is different, and will have different combinations of phenomena that are expected to occur. Being that we are developing a model for a CSTR, we would expect there to be some form of chemical reaction occurring within our unit, as well as the possibility of a heating or cooling jacket. Among the construction arguments in the default config block are the following:
- has_rate_reactions,
- has_equilibrium reactions, and,
- has_heat_transfer.
Each of these construction arguments are used to tell the unit model (and any associated Holdup blocks) whether or not to include these terms in the material and energy balances. Seeing as we expect these phenomena to be present in our unit, let’s set the default value for these arguments to True.
CONFIG.get("has_rate_reactions")._default = True
CONFIG.get("has_equilibrium_reactions")._default = True
CONFIG.get("has_heat_transfer")._default = True
Property Package Arguments¶
The next thing we need to do is add some extra arguments to the config block for our new class. One important thing that is not included in the default config block are arguments for providing information on property blocks to the unit model. The reason for this is that some unit models may involve more than one material (e.g. heat exchangers), and thus require multiple property packages. Unit models require two arguments for each property package which they will use:
- a reference to a Property Parameter Block, and,
- a set of construction arguments to pass onto the Property Block when it is constructed (generally optional).
Our CSTR only requires one Property Package, so let’s create two new entries in the config block. Each new entry needs to be given a unique name, and can also be given a default value, a domain and a set of documentation strings (long and/or short). The domain is a callable object of some type that can be used to validate the value provided by the user, thus ensuring a meaningful value is provided. For our example, let us add the following two arguments:
CONFIG.declare("property_package", ConfigValue(
default=None,
domain=is_parameter_block,
description="Property package to use for holdup")
CONFIG.declare("property_package_args", ConfigValue(
default={},
description="Arguments to use for constructing property packages")
In the above code, we first declare a new entry in our config block and provide a name for the argument (e.g. “property_package”). We then need to specify the type of entry, which in this case is a “ConfigValue” - for more information on the different types available see the Pyomo documentation for config blocks. We can then provide a default value for the argument, and set the domain and documentation strings. In our example, we set the domain for “property_package” to is_parameter_block, which is an IDAES method that checks that the value provided by the user points to a valid Property Parameter Block.
Accessing Construction Arguments¶
Now that we have set up our config block the next question is how do we make use of it? When we create an instance of a class, the IDAES framework automatically passes any arguments provided in the object declaration to the new objects config block so they are available for use in the object. So, in order to make use of the construction arguments, all we need to do is look to object.config.argument_name to get the current value of the construction argument.
The build Method¶
After setting up our config block, the next step is to write the build method for our new class, and to then call the UnitModelData build method (see the documentation on Unit Model classes for information on what is performed by this method).
def build(self):
super(CSTRData, self).build()
Holdup Blocks¶
Once we have the basic framework of our unit model set up, the next thing we should do is add some Holdup blocks to represent any control volumes we have in our model. The Holdup Blocks take a set of construction arguments similar to those in CONFIG_Base, and write a set of material, energy and momentum balances based on these arguments (see documentation on Holdup Blocks for more details). For a CSTR, we have a single well-mixed control volume representing the material in the tank, so we can use a single Holdup0D to represent our system.
self.holdup = Holdup0D()
In this case, we do not need to provide any arguments to the Holdup block when we create it - if we do not provide any arguments the Holdup block will automatically look to the unit model’s config block for its construction arguments.
Adding Performance Equations¶
Adding the Holdup block to our model automatically generates the material, energy and momentum balances we need, so all that is left is to write some Constraints describing how the unit performs. For a CSTR, the key performance equation is the relationship between the extent of reaction, rate of reaction and the volume of the tank:
where \(X_{t,r}\) is the extent of reaction of reaction \(r\) at time \(t\), \(V_t\) is the volume of the reacting material at time \(t\) (allows for varying reactor volume with time) and \(r_{t,r}\) is the volumetric rate of reaction of reaction \(r\) at time \(t\).
In order to implement this Constraint, the first thing we are going to need is the volume material in the tank. One of the options for the Holdup block is include_holdup, which tells the Holdup block to calculate the holdup of material and energy within the control volume. This of requires the Holdup block to have a volume, so we should make use of this if it is present (i.e. if include_holdup is True). Otherwise, we need to create a new variable for volume. We do this as shown below:
if self.config.include_holdup:
add_object_ref(self, "volume", self.holdup.volume)
else:
# Add a volume variable, as CSTRs always need a volume
self.volume = Var(self.time,
initialize=1.0,
doc="Reactor volume")
Next, we need the extent of reaction and rate of reaction terms to write our Constraint. Rate of reaction is a property of the state of the material and thus can be found in the outlet Property Block, whilst the Holdup Block automatically generates an extent of reaction term for each reaction when the has_rate_reaction construction argument is True (which we set earlier). Let us create a reference to the extent of reaction in our unit model for convenience:
add_object_ref(self, "rate_reaction_idx", self.holdup.rate_reaction_idx)
Now that we have the terms we need, let’s write our performance Constraint (indexed across all points in time so it can handle dynamic problems).
def rate_reaction_extents(b, t, k):
return b.holdup.rate_reaction_extent[t, k] == (
b.volume[t] *
b.holdup.properties_out[t].reaction_rate[k])
self.rate_reaction_extents = Constraint(self.time,
self.rate_reaction_idx,
doc="Extents of reaction")
Whilst it might not seem like much, that is all we need to write to create the Constraints necessary to describe a CSTR. A lot of the work is being done automatically for us by the Holdup block (such as setting up property calculations, writing material, energy and momentum balances), so we as model developers only need to provide a set of instruction on what to build and a few important Constraints to describe the performance of our unit.
The post_transform_build Method¶
As has been discussed in previous tutorials, there are some parts of model construction that cannot be completed until after all model transformations have been applied. The most significant of these is the construction of the inlet and outlet Port objects which are used to connect unit models together. In order to facilitate this, any steps that need to be performed after transformations are applied are placed in a separate method in the model class named post_transform_build, which can be called by the flowsheet model.
build_inlets and build_outlets Methods¶
In order to simplify the construction of the inlet and outlet Port objects, the IDAES UnitBlockData class contains two methods which automate this for the user. These methods have three arguments which describe the Port object to be constructed.
- holdup - indicates which Holdup block the Port should be associated with. If not specified, the methods assume there is a Holdup block with the name holdup.
- an optional list of names to use if multiple Streams are to be connected to a single Port (with inherent mixing/splitting of the flows).
- an optional number of Streams that will be connected to the Port (with inherent mixing/splitting).
Only one of the list of names or number of Streams needs to be specified (and an Exception will be raised if both are provided and are not consistent). More documentation on the build_inlets and build_outlets methods can be found in the Unit Models documentation.
Writing Initialization Routines¶
To be added once the new initialization framework is completed.
Writing Model Checks¶
To be added once the new initialization framework is completed (as we might overhaul this at the same time).