Module 1 Flash Unit Solution:¶
Module 1: Flash Unit¶
In this module, we will familiarize ourselves with the IDAES framework by creating and working with a flowsheet that contains a single flash tank. The flash tank will be used to perform separation of Benzene and Toluene. The inlet specifications for this flash tank are:
Inlet Specifications:
- Mole fraction (Benzene) = 0.5
- Mole fraction (Toluene) = 0.5
- Pressure = 101325 Pa
- Temperature = 368 K
We will complete the following tasks:
- Create the model and the IDAES Flowsheet object
- Import the appropriate property packages
- Create the flash unit and set the operating conditions
- Initialize the model and simulate the system
- Demonstrate analyses on this model through some examples and exercises
Key links to documentation¶
- Main IDAES online documentation page: https://idaes-pse.readthedocs.io/en/latest/index.html
- Core IDAES Library: https://idaes-pse.readthedocs.io/en/latest/core/index.html
- Modeling Standards: https://idaes-pse.readthedocs.io/en/latest/standards.html
- Naming Conventions: https://idaes-pse.readthedocs.io/en/latest/standards.html#standard-naming-format
- IDAES Unit Model Library: https://idaes-pse.readthedocs.io/en/latest/models/index.html
Create the Model and the IDAES Flowsheet¶
In the next cell, we will perform the necessary imports to get us started. From pyomo.environ
(a standard import for the Pyomo package), we are importing ConcreteModel
(to create the Pyomo model that will contain the IDAES flowsheet) and SolverFactory
(to create the object we will use to solve the equations). We will also import Constraint
as we will be adding a constraint to the model later in the module. Lastly, we also import value
from Pyomo. This is a function that can be used to return the current numerical value for variables and parameters in the model. These are all part of Pyomo.
We will also import the main FlowsheetBlock
from IDAES. The flowsheet block will contain our unit model.
from pyomo.environ import ConcreteModel, SolverFactory, Constraint, value
from idaes.core import FlowsheetBlock
In the next cell, we will create the ConcreteModel
and the FlowsheetBlock
, and attach the flowsheet block to the Pyomo model.
m = ConcreteModel()
m.fs = FlowsheetBlock(default={"dynamic": False})
At this point, we have a single Pyomo model that contains an (almost) empty flowsheet block.
# Todo: call pprint on the model
m.pprint()
Define Properties¶
We need to define the property package for our flowsheet. In this example, we have created a property package based on ideal VLE that contains the necessary components.
IDAES supports creation of your own property packages that allow for specification of the fluid using any set of valid state variables (e.g., component molar flows vs overall flow and mole fractions). This flexibility is designed to support advanced modeling needs that may rely on specific formulations. As well, the IDAES team has completed some general property packages (and is currently working on more). To learn about creating your own property package, please consult the online documentation at: https://idaes-pse.readthedocs.io/en/latest/core/properties.html and look at examples within IDAES
For this workshop, we will import the BTX_ideal_VLE property package and create a properties block for the flowsheet. This properties block will be passed to our unit model to define the appropriate state variables and equations for performing thermodynamic calculations.
import BTX_ideal_VLE as ideal_props
m.fs.properties = ideal_props.BTXParameterBlock()
Adding Flash Unit¶
Now that we have the flowsheet and the properties defined, we can create the flash unit and add it to the flowsheet.
The Unit Model Library within IDAES includes a large set of common unit operations (see the online documentation for details: https://idaes-pse.readthedocs.io/en/latest/models/index.html
IDAES also fully supports the development of customized unit models (which we will see in a later module).
Some of the IDAES pre-written unit models:
- Mixer / Splitter
- Heater / Cooler
- Heat Exchangers (simple and 1D discretized)
- Flash
- Reactors (kinetic, equilibrium, gibbs, stoichiometric conversion)
- Pressure changing equipment (compressors, expanders, pumps)
- Feed and Product (source / sink) components
In this module, we will import the Flash
unit model from idaes.unit_models
and create an instance of the flash unit, attaching it to the flowsheet. Each IDAES unit model has several configurable options to customize the model behavior, but also includes defaults for these options. In this example, we will specify that the property package to be used with the Flash is the one we created earlier.
from idaes.unit_models import Flash
m.fs.flash = Flash(default={"property_package": m.fs.properties})
At this point, we have created a flowsheet and a properties block. We have also created a flash unit and added it to the flowsheet. Under the hood, IDAES has created the required state variables and model equations. Everything is open. You can see these variables and equations by calling the Pyomo method pprint
on the model, flowsheet, or flash tank objects. Note that this output is very exhaustive, and is not intended to provide any summary information about the model, but rather a complete picture of all of the variables and equations in the model.
Set Operating Conditions¶
Now that we have created our unit model, we can specify the necessary operating conditions. It is often very useful to determine the degrees of freedom before we specify any conditions.
The idaes.core.util.model_statistics
package has a function degrees_of_freedom
. To see how to use this function, we can make use of the Python function help(func)
. This function prints the appropriate documentation string for the function.
# Todo: import the degrees_of_freedom function from the idaes.core.util.model_statistics package
from idaes.core.util.model_statistics import degrees_of_freedom
# Todo: Call the python help on the degrees_of_freedom function
help(degrees_of_freedom)
# Todo: print the degrees of freedom for your model
print("Degrees of Freedom =", degrees_of_freedom(m))
To satisfy our degrees of freedom, we will first specify the inlet conditions. We can specify these values through the inlet
port of the flash unit.
To see the list of naming conventions for variables within the IDAES framework, consult the online documentation at: https://idaes-pse.readthedocs.io/en/latest/standards.html#standard-naming-format
As an example, to fix the molar flow of the inlet to be 1.0, you can use the following notation:
m.fs.flash.inlet.flow_mol.fix(1.0)
To specify variables that are indexed by components, you can use the following notation:
m.fs.flash.inlet.mole_frac[0, "benzene"].fix(0.5)
In the next cell, we will specify the inlet conditions. To satisfy the remaining degrees of freedom, we will make two additional specifications on the flash tank itself. The names of the key variables within the Flash unit model can also be found in the online documentation: https://idaes-pse.readthedocs.io/en/latest/models/flash.html#variables.
To specify the value of a variable on the unit itself, use the following notation.
m.fs.flash.heat_duty.fix(0)
For this module, we will use the following specifications:
- inlet overall molar flow = 1.0 (
flow_mol
) - inlet temperature = 368 K (
temperature
) - inlet pressure = 101325 Pa (
pressure
) - inlet mole fraction (benzene) = 0.5 (
mole_frac[0, "benzene"]
) - inlet mole fraction (toluene) = 0.5 (
mole_frac[0, "toluene"]
) - The heat duty on the flash set to 0 (
heat_duty
) - The pressure drop across the flash tank set to 0 (
deltaP
)
# Inlet specifications given above
m.fs.flash.inlet.flow_mol.fix(1)
m.fs.flash.inlet.temperature.fix(368)
m.fs.flash.inlet.pressure.fix(101325)
m.fs.flash.inlet.mole_frac[0, "benzene"].fix(0.5)
m.fs.flash.inlet.mole_frac[0, "toluene"].fix(0.5)
# Todo: add code for the 2 flash unit specifications given above
m.fs.flash.heat_duty.fix(0)
m.fs.flash.deltaP.fix(0)
# Todo: print the degrees of freedom for your model
print("Degrees of Freedom =", degrees_of_freedom(m))
Initializing the Model¶
IDAES includes pre-written initialization routines for all unit models. You can call this initialize method on the units. In the next module, we will demonstrate the use of a sequential modular solve cycle to initialize flowsheets.
# Todo: initialize the flash unit
m.fs.flash.initialize()
Now that the model has been defined and intialized, we can solve the model.
# Todo: create the ipopt solver
solver = SolverFactory('ipopt')
# Todo: solve the model
status = solver.solve(m, tee=True)
# For testing purposes
from pyomo.environ import TerminationCondition
assert status.solver.termination_condition == TerminationCondition.optimal
Viewing the Results¶
Once a model is solved, the values returned by the solver are loaded into the model object itself. We can access the value of any variable in the model with the value
function. For example:
print('Vap. Outlet Temperature = ', value(m.fs.flash.vap_outlet.temperature[0]))
You can also find more information about a variable or an entire port using the display
method from Pyomo:
m.fs.flash.vap_outlet.temperature.display()
m.fs.flash.vap_outlet.display()
# Print the pressure of the flash vapor outlet
print('Pressure =', value(m.fs.flash.vap_outlet.pressure[0]))
print()
print('Output from display:')
# Call display on vap_outlet and liq_outlet of the flash
m.fs.flash.vap_outlet.display()
m.fs.flash.liq_outlet.display()
The output from display
is quite exhaustive and not really intended to provide quick summary information. Because Pyomo is built on Python, there are opportunities to format the output any way we like. Most IDAES models have a report
method which provides a summary of the results for the model.
m.fs.flash.report()
Studying Purity as a Function of Heat Duty¶
Since the entire modeling framework is built upon Python, it includes a complete programming environment for whatever analysis we may want to perform. In this next exercise, we will make use of what we learned in this and the previous module to generate a figure showing some output variables as a function of the heat duty in the flash tank.
First, let's import the matplotlib package for plotting as we did in the previous module.
import matplotlib.pyplot as plt
Exercise specifications:
- Generate a figure showing the flash tank heat duty (
m.fs.flash.heat_duty[0]
) vs. the vapor flowrate (m.fs.flash.vap_outlet.mol_flow[0]
) - Specify the heat duty from -17000 to 25000 over 20 steps
# import the solve_successful checking function from workshop tools
from workshoptools import solve_successful
# Todo: import numpy
import numpy as np
# create the empty lists to store the results that will be plotted
Q = []
V = []
# create the solver
solver = SolverFactory('ipopt')
# Todo: Write the for loop specification using numpy's linspace
for duty in np.linspace(-17000, 25000, 20):
# fix the heat duty
m.fs.flash.heat_duty.fix(duty)
# append the value of the duty to the Q list
Q.append(duty)
# print the current simulation
print("Simulating with Q = ", value(m.fs.flash.heat_duty[0]))
# Solve the model
status = solver.solve(m)
# append the value for vapor fraction if the solve was successful
if solve_successful(status):
V.append(value(m.fs.flash.vap_outlet.flow_mol[0]))
print('... solve successful.')
else:
V.append(0.0)
print('... solve failed.')
# Create and show the figure
plt.figure("Vapor Fraction")
plt.plot(Q, V)
plt.grid()
plt.xlabel("Heat Duty [J]")
plt.ylabel("Vapor Fraction [-]")
plt.show()
# Todo: generate a figure of heat duty vs. mole fraction of Benzene in the vapor
Q = []
V = []
for duty in np.linspace(-17000, 25000, 20):
# fix the heat duty
m.fs.flash.heat_duty.fix(duty)
# append the value of the duty to the Q list
Q.append(duty)
# solve the model
status = solver.solve(m)
# append the value for vapor fraction if the solve was successful
if solve_successful(status):
V.append(value(m.fs.flash.vap_outlet.mole_frac[0, "benzene"]))
else:
V.append(0.0)
print('... solve failed.')
plt.figure("Purity")
plt.plot(Q, V)
plt.grid()
plt.xlabel("Heat Duty [J]")
plt.ylabel("Vapor Benzene Mole Fraction [-]")
plt.show()
Recall that the IDAES framework is an equation-oriented modeling environment. This means that we can specify "design" problems natively. That is, there is no need to have our specifications on the inlet alone. We can put specifications on the outlet as long as we retain a well-posed, square system of equations.
For example, we can remove the specification on heat duty and instead specify that we want the mole fraction of Benzene in the vapor outlet to be equal to 0.6. The mole fraction is not a native variable in the property block, so we cannot use "fix". We can, however, add a constraint to the model.
Note that we have been executing a number of solves on the problem, and may not be sure of the current state. To help convergence, therefore, we will first call initialize, then add the new constraint and solve the problem. Note that the reference for the mole fraction of Benzene in the vapor outlet is m.fs.flash.vap_outlet.mole_frac[0, "benzene"]
.
# unfix the heat duty
m.fs.flash.heat_duty.unfix()
# re-initialize the model - this may or may not be required depending on current state
m.fs.flash.initialize()
# Todo: Add a new constraint (benzene mole fraction to 0.6)
m.benz_purity_con = Constraint(expr= m.fs.flash.vap_outlet.mole_frac[0, "benzene"] == 0.6)
# solve the problem
status = solver.solve(m, tee=True)
# print the value of the heat duty
print('Q =', value(m.fs.flash.heat_duty[0]))
# For testing purposes
from pyomo.environ import TerminationCondition
assert status.solver.termination_condition == TerminationCondition.optimal