Tutorial 1 - Basic Flowsheets¶
Introduction¶
For this tutorial on how to construct simple flowsheets within the IDAES framework, we will use a simple steady-state process of two well-mixed reactors (CSTRs) in series, with the following system reactions occurring in each.

In this tutorial, you will learn how to:
- import the necessary libraries from Pyomo and IDAES
- create a flowsheet object
- add a property package to a flowsheet
- add unit models to a flowsheet
- connect units together
- set inlet conditions and design variables
- initialize a model
- solve the model and print some results
First Steps¶
The first thing we need to do is import some components from Pyomo which will be used in our model:
- ConcreteModel will be used to form the basis of our model, and
- SolverFactory will be used to solve our flowsheet.
All of these can be imported from pyomo.environ using the following format:
from pyomo.environ import ConcreteModel, SolverFactory
Next, we need to import FlowsheetBlock and Stream from the ideas.core. FlowsheetBlock used as the basis for constructing all flowsheets in IDAES, and provides the necessary infrastructure for constructing a flowsheet, whilst Stream is used to connect different Unit Models together.
from idaes.core import FlowsheetBlock, Stream
Finally, we need to import models for the reactors and the properties calculations we wish to use. For this tutorial we will be using the CSTR model available in the IDAES model library, and a set of property calculations that has been prepared and is available at idaes/examples/core/tutorials/tutorial_1_properties.py. We will also import and use the IDAES Feed and Product Blocks to use in our flowsheet.
These can be imported with the following code:
import tutorial_1_properties as properties_rxn
from idaes.models import Feed, CSTR, Product
Setting up a model and flowsheet¶
Now that we have imported the necessary libraries, we can begin constructing the model of our process. The first step in constructing a model within the IDAES framework is to create a ConcreteModel with which to contain the flowsheet. This is the same as creating a ConcreteModel within Pyomo.
m = ConcreteModel()
Next, we add a FlowsheetBlock object to our model. FlowsheetBlock is an IDAES modelling object which contains all the attributes required of a flowsheet within the IDAES framework (such as the time domain for dynamic models). FlowsheetBlock supports a number of different options, which can be set by providing arguments when the block is instantiated. For the current example we want a steady-state flowsheet, which is done by setting the argument dynamic=False.
To add a flowsheet named fs to our model, we do the following:
m.fs = FlowsheetBlock(dynamic=False)
This creates a flowsheet block within our model which we can now start to populate with property calculations and unit models. As unit models depend on property packages for part of their construction, we need to add the property package to our flowsheet first. This is done by creating an instance of a PropertyParameterBlock, which is part of all IDAES property packages. This block contains all the information required to set up a property package for our flowsheet, and can be passed to a unit model in order to set up the property calculations within that model.
To add a property package with the name properties_rxn to our Flowsheet, we do the following:
m.fs.properties_rxn = properties_rxn.PropertyParameterBlock()
The example property package for this tutorial is fairly simple, and does not need any arguments; however more complex packages may have options that can be set during construction. Users should always refer to the documentation for any property package they are using to understand any available options. It is also possible for a flowsheet to contain multiple property packages, which can be used in different parts of the flowsheet.
The next thing we need for our Flowsheet is a source of material entering the process, for which we will use a Feed Block. For now, we will just create the Feed Block, and we will come back later to specify the conditions of the incoming material. When we create our Feed Block, we need to pass the property package we wish to use for the mateiral stream to the unit model as an argument.
m.fs.Feed = Feed(property_package=m.fs.properties_rxn)
Next, we can add our two CSTRs to the flowsheet, which we will call Tank1 and Tank2. In both cases, we again need to pass the property package to the unit model as an argument. Unit models can also take a number of other arguments, however these will be covered in later tutorials.
To add a unit model to a flow sheet, create an instance of the model and pass any desired arguments to it. For example:
m.fs.Tank1 = CSTR(property_package=m.fs.properties_rxn)
m.fs.Tank2 = CSTR(property_package=m.fs.properties_rxn)
Finally, let us add a Product Block to serve as a marker for the final state of the material. Just like for Feed and CSTR, we need to provide the property package argument.
m.fs.Product = Product(property_package=m.fs.properties_rxn)
Feed and Product Blocks are not necessary in flowsheets, and a fucntional flowsheet can be built without them. However, these serve as convenient markers for sources and destinations of material within the process.
The final stage in setting up the components in a flowsheet is to call post_transform_build. Due to some current limitations in Pyomo (hopefully to be fixed in Pyomo 5.5), there are some parts of IDAES models that cannot be constructed until after any DAE transformations have been applied. post_transform_build is a method attached to FlowsheetBlock which automatically goes through all unit models attached to the flowsheet and performs any tasks that must be performed after DAE transformations (more on this in later tutorials). This is most commonly associated with the time domain in dynamic models, and is not required in the current model. Thus, for steady-state models, post_transform_build can be called immediately after the unit models have been declared.
m.fs.post_transform_build()
Connecting Units¶
Now that we have added our unit models to the flowsheet and called post_transform_build on our flowsheet, we can begin connecting units together. Connections need to be made after post_transform_build is called, as these are some of the things that cause problems with Pyomo’s DAE transformation. Each unit model will contain a number of inlets and outlets Port objects, which can be connected using IDAES Stream objects. In our flowsheet, each unit has a single inlet and a single outlet, named inlet and outlet respectively. For our flowsheet, we need to connect the following:
- outlet of Feed to the inlet of Tank1 (Stream 1),
- outlet of Tank1 to the inlet of Tank2 (Stream 2),
- outlet of Tank2 to the inlet of Product (Stream 3).
m.fs.stream_1 = Stream(source=m.fs.Feed.outlet, destination=m.fs.Tank1.inlet)
m.fs.stream_2 = Stream(source=m.fs.Tank1.outlet, destination=m.fs.Tank2.inlet)
m.fs.stream_3 = Stream(source=m.fs.Tank2.outlet, destination=m.fs.Product.inlet)
At this point, we have finished constructing our flowsheet, and can now move onto specifying our operating conditions and solving the model.
Setting Operating Conditions¶
In setting the operating conditions, the first thing we need to specify are the inlet conditions to process, which can be done through the Feed Block. For our model, we need to specify flow rates of each component (a through f) as well as the pressure and temperature of the inlet stream.
The conditions we need to fix are:
- flow_mol_comp[“a”] = 1.0 [mol/s]
- flow_mol_comp[“b”] = 2.0 [mol/s]
- flow_mol_comp[“c”] = 0.1 [mol/s]
- flow_mol_comp[“d”] = 0.0 [mol/s]
- flow_mol_comp[“e”] = 0.0 [mol/s]
- flow_mol_comp[“f”] = 0.0 [mol/s]
- temperature = 303.15 [K]
- pressure = 101325.0 [Pa]
m.fs.Feed.fix('flow_mol_comp', comp='a', value=1.0)
m.fs.Feed.fix('flow_mol_comp', comp='b', value=2.0)
m.fs.Feed.fix('flow_mol_comp', comp='c', value=0.1)
m.fs.Feed.fix('flow_mol_comp', comp='d', value=0.0)
m.fs.Feed.fix('flow_mol_comp', comp='e', value=0.0)
m.fs.Feed.fix('flow_mol_comp', comp='f', value=0.0)
m.fs.Feed.fix('temperature', value=303.15)
m.fs.Feed.fix('pressure', value=101325)
Additionally, we need to specify some design conditions for the system – in this case the volume and heat of both tanks. Let us fix the volume of each tank to be 10 \(m^3\) and the heat duty to be 0 J/s. The variable names are “volume” and “heat”.
m.fs.Tank1.volume.fix(10.0)
m.fs.Tank1.heat.fix(0.0)
m.fs.Tank2.volume.fix(10.0)
m.fs.Tank2.heat.fix(0.0)
Initializing and Solving the Model¶
Now that the model has been constructed and the inlet and design conditions have been specified, we can now work on solving the model. However, most process engineering models cannot be solved in a single step, and require some degree of initialization to get to a solvable state. The models within the IDAES model library contain prebuilt initialization routines which can be used to get each model to a solvable state. For this tutorial, we will use a sequential modular type approach to initializing our flowsheet using these prebuilt methods.
We will begin with initializing the Feed Block, as it is the first unit in our flowsheet. The initialization routine for a Feed Block expects the conditions of the inlet stream to be provided as initial guesses along with any design conditions required to have zero degrees of freedom. However, as the conditions for the Feed are specified (fixed), there is no need to provide additional guesses for these and the initialization routine will make use of the specified value automatically.
The initialization routines also require a non-linear solver to be available to solve the model. This tutorial assumes that you have IPOPT installed, however you can substitute this for other NLP solvers you may have available. In order to do this, you can set the solver keyword when calling the initialization routine with the name of your NLP solver (e.g. solver=’ipopt’).
m.fs.Feed.initialize()
Now that the Feed has been initialized, we can use the outlet conditions for the initial guesses for the inlet of Tank1. These are provided to the initialization routine through the state_args argument (which is a Python dict). To get the values from the outlet of Feed, we can use the outlet Port object, using the stream keys to access each variable. Note that we need to specify a time index for the outlet Port, which for a steady-state model is just 0. Additionally, we need to use the Pyomo value method to get the actual value of the variable in question.
m.fs.Tank1.initialize(state_args={
"flow_mol_comp": {
"a": m.fs.Feed.outlet[0].vars["flow_mol_comp"]["a"].value,
"b": m.fs.Feed.outlet[0].vars["flow_mol_comp"]["b"].value,
"c": m.fs.Feed.outlet[0].vars["flow_mol_comp"]["c"].value,
"d": m.fs.Feed.outlet[0].vars["flow_mol_comp"]["d"].value,
"e": m.fs.Feed.outlet[0].vars["flow_mol_comp"]["e"].value,
"f": m.fs.Feed.outlet[0].vars["flow_mol_comp"]["f"].value},
"pressure": m.fs.Feed.outlet[0].vars["pressure"].value,
"temperature": m.fs.Feed.outlet[0].vars["temperature"].value})
We can then repeat this process for Tank2.
m.fs.Tank2.initialize(state_args={
"flow_mol_comp": {
"a": m.fs.Tank1.outlet[0].vars["flow_mol_comp"]["a"].value,
"b": m.fs.Tank1.outlet[0].vars["flow_mol_comp"]["b"].value,
"c": m.fs.Tank1.outlet[0].vars["flow_mol_comp"]["c"].value,
"d": m.fs.Tank1.outlet[0].vars["flow_mol_comp"]["d"].value,
"e": m.fs.Tank1.outlet[0].vars["flow_mol_comp"]["e"].value,
"f": m.fs.Tank1.outlet[0].vars["flow_mol_comp"]["f"].value},
"pressure": m.fs.Tank1.outlet[0].vars["pressure"].value,
"temperature": m.fs.Tank1.outlet[0].vars["temperature"].value})
At this point, our model should now be initialized and ready to solve. In order to do this, we need to create a solver using Pyomo’s SolverFactory, which is done the same way in IDAES as in Pyomo.
solver = SolverFactory('ipopt')
Just like in Pyomo, solver options can be provided as well by attaching a dictionary of keywords to the solver object, however these are not needed for this tutorial. Once the solver object is created, we can call it to solve the model and return the results object.
results = solver.solve(m, tee=True)
Let us print the results object to get more details about our solution.
print(results)
Finally, let’s display the Product Block so we can see what the conditions of the product stream are.
m.fs.Product.display()
Hopefully you get an output reporting “Optimal Solution Found”. You should also see that there were 154 variables and constraints in the problem. If the number of constraints and variables are not equal, check that you specifed all the required inputs (there should be 12 variables which were fixed). You might also see some warnings at the begining about equilibrium reaction, but these can be ignored.
If all has gone well, you should see the following conditions in the Product Block.
- flow_mol_comp[“a”] = 0.216 [mol/s]
- flow_mol_comp[“b”] = 0.978 [mol/s]
- flow_mol_comp[“c”] = 0.332 [mol/s]
- flow_mol_comp[“d”] = 0.244 [mol/s]
- flow_mol_comp[“e”] = 0.011 [mol/s]
- flow_mol_comp[“f”] = 0.534 [mol/s]
- pressure = 101325 [Pa]
- temperature = 402.15 [K]
Moving On¶
The next tutorial will build on this one, so it is recommended that you save this tutorial so you don’t have to rewrite the model code.