Tutorial 2 - Basic Flowsheet Optimization¶
Introduction¶
In the previous tutorial, we developed a model for a simple flowsheet to simulate the performance of a series of reactions occurring in two CSTRs in series. In this tutorial, we will move from simulating to optimizing the flowsheet by trying to improve the yield of specific components.

In this tutorial, you will learn how to:
- add an objective function to a model,
- set bounds on variables,
- add degrees of freedom to the model, and
- solve the resulting optimization problem.
First Steps¶
The first step in setting up an optimization problem is to create and a flowsheet for the problem and initialize it. Given that most problems of interest to engineers are complex and highly non-linear, it is general best to start with a fully defined problem and solve it for an initial set of conditions (from which we will try to find the optimal solution).
Hopefully you saved the file from the previous tutorial which we will extend to include optimization - otherwise please refer to Tutorial 1 for instructions on how to construct the flowsheet.
Next Steps¶
Next, we need to import a couple of additional things from Pyomo in order to create and optimization problem:
- Objective will be used to define our objective function, and
- minimize will be used to tell our solver that it needs to minimize the objective.
These can be added to the existing import from pyomo.enivron at the beginning of our file.
from pyomo.environ import ConcreteModel, SolverFactory, Objective, minimize
Adding an Objective Function¶
Now that we have an initialized flowsheet for our problem, we can go about adding an objective function. For this tutorial, let us consider component “f” to be an undesired side product of our reactions, and try to minimize to amount of this component produced.
For this, we add an Objective object to our flowsheet (m.fs as you may recall) and provide it with an expression for the objective function (in this case the flowrate of component f leaving Tank2) and an instruction on whether to minimize and maximize this expression.
m.fs.obj = Objective(expr=m.fs.Tank2.outlet[0].vars["flow_mol_comp"]["f"],
sense=minimize)
Adding Variable Bounds¶
Next, we need to add some limits on our problem to make sure the results of the optimization are physically reasonable. For this problem, we will add some bounds on the temperatures in both tanks, to make sure that they do not get too hot or too cold. For this tutorial, let us set a lower bound on temperature in both tanks to be 300 [K], and an upper bound of 400 [K] in Tank1 and 450 [K] in Tank2.
As we are using CSTRs for our reactors in this tutorial, the temperatures within the tanks are equal to the temperatures in the outlets, so we can apply our bounds to the outlet temperatures. We can set the upper and lower bounds of a variable by using the Pyomo setub and setlb methods as shown below.
m.fs.Tank1.outlet[0].vars["temperature"].setlb(300)
m.fs.Tank2.outlet[0].vars["temperature"].setlb(300)
m.fs.Tank1.outlet[0].vars["temperature"].setub(400)
m.fs.Tank2.outlet[0].vars["temperature"].setub(450)
Adding Degrees of Freedom¶
Finally, we need to provide our problem with some degrees of freedom that the solver can use to try to find the optimal solution to our problem. The IDAES CSTR model allows for the addition or removal of heat from the reactor, so let’s use the reactor heat duties as our degrees of freedom for this problem.
When we were initializing the problem, we specified that the reactor heat duties were fixed variables with a value of 0 [J/s] to make our problem fully defined. We can now unfix these variables to allow the solver to manipulate them, and thus adjust the temperatures in the reactors.
m.fs.Tank1.heat.unfix()
m.fs.Tank2.heat.unfix()
Solving the Optimization Problem¶
Now that our optimization problem is fully defined, we can go ahead and try to solve it. We should already have a solver object defined within our flowsheet from Tutorial 1, so we can now apply it to the optimization problem to find our optimal solution.
results = solver.solve(m, tee=True)
Once again, let us print the results object to get more details about our solution.
print(results)
Hopefully we will see that our problem has 154 constraints and 156 variables (thus two degrees of freedom), and that an optimal solution was found.
Finally, let’s display the Product Block and reactor heat duties so we can see what solution the optimizer found.
m.fs.Product.display()
m.fs.Tank1.heat.display()
m.fs.Tank2.heat.display()
If all has gone well, you should see the following conditions in the Product Block.
- flow_mol_comp[“a”] = 0.497 [mol/s]
- flow_mol_comp[“b”] = 1.315 [mol/s]
- flow_mol_comp[“c”] = 0.279 [mol/s]
- flow_mol_comp[“d”] = 0.184 [mol/s]
- flow_mol_comp[“e”] = 0.006 [mol/s]
- flow_mol_comp[“f”] = 0.316 [mol/s]
- pressure = 101325 [Pa]
- temperature = 300.0 [K]
You should also see the following heat duties:
- Tank1 = -7364.4 [J/s]
- Tank2 = -4201.7 [J/s]
Compared to Tutorial 1, we can see that the amount of component “f” being produced has dropped from 0.534 [mol/s]. This has been achieved by reducing the temperature in both reactors by removing heat, and limiting the temperature in the system to the lower bound (300 [K]). This makes sense, as all the formation of component “f” in our example is exothermic, so reducing temperature will reduce the yield of this component. However, the other reactions are also exothermic, so we also see reduced yields of all other components.