How to setup simple chemistry
Note
This page provides a manual approach to building an IDAES configuration dictionary.
In WaterTAP, chemistry modules leverage the Generic Properties (GenericProperties) and Generic Reactions (GenericReactions) objects in IDAES. These objects can be used in conjunction with any unit process where chemical reactions need to be considered. In this guide, we will cover how to use these built-in objects within your own unit process.
What you will need
[Required] A thermo-properties configuration dictionary
[Optional] A reaction-properties configuration dictionary
[Required] A unit model upon which to build the chemistry module
Note
The reaction-properties dictionary is optional for certain unit models and required for others. Additionally, certain reactions can actually be defined in the thermo-properties dictionary and would therefore NOT be included in the reaction-properties dictionary. The differences will be covered in another how-to guide (see How to use inherent reactions)
The thermo-properties configuration dictionary
You will always be required to provide a configuration dictionary for thermodynamic properties of the chemical species of interest in your process. At a minimum, the thermo-properties dictionary MUST contain the following keys…
Key |
Description |
---|---|
components |
dictionary containing the chemical species of interest and their properties |
phases |
dictionary containing the phases (gas, liquid, etc) for the system and its species |
state_definition |
IDAES object which defines how inlet/outlet ports define the initial state of the system |
base_units |
dictionary containing the base units the model will use for all equations in the system |
Note
Depending on your system, there may be additional keys in the thermo-properties configuration dictionary that may be required. For instance, multi-phase problems also require you to define how the model should represent the phase equilibria between the system’s phases. An in depth discussion of all options and methods is beyond the scope of this guide. For additional information, refer to the IDAES documentation (GenericProperties).
Example thermo-properties configuration dictionary
In this example, we will define a simple thermo-properties configuration dictionary for a chemical system that contains only water.
# Importing the object for units from pyomo
from pyomo.environ import units as pyunits
# Imports from idaes core
from idaes.core import AqueousPhase
from idaes.core.base.components import Solvent, Cation, Anion
# Imports from idaes generic models
import idaes.models.properties.modular_properties.pure.Perrys as Perrys
from idaes.models.properties.modular_properties.pure.ConstantProperties import Constant
from idaes.models.properties.modular_properties.state_definitions import FTPx
from idaes.models.properties.modular_properties.eos.ideal import Ideal
# Configuration dictionary
thermo_config = {
"components": {
'H2O': {"type": Solvent,
# Define the methods used to calculate the following properties
"dens_mol_liq_comp": Perrys,
"enth_mol_liq_comp": Perrys,
"cp_mol_liq_comp": Perrys,
"entr_mol_liq_comp": Perrys,
# Parameter data is always associated with the methods defined above
"parameter_data": {
"mw": (18.0153, pyunits.g/pyunits.mol),
# Parameters here come from Perry's Handbook: p. 2-98
"dens_mol_liq_comp_coeff": {
'eqn_type': 1,
'1': (5.459, pyunits.kmol*pyunits.m**-3),
'2': (0.30542, pyunits.dimensionless),
'3': (647.13, pyunits.K),
'4': (0.081, pyunits.dimensionless)},
"enth_mol_form_liq_comp_ref": (-285.830, pyunits.kJ/pyunits.mol),
"enth_mol_form_vap_comp_ref": (0, pyunits.kJ/pyunits.mol),
# Parameters here come Perry's Handbook: p. 2-174
"cp_mol_liq_comp_coeff": {
'1': (2.7637E5, pyunits.J/pyunits.kmol/pyunits.K),
'2': (-2.0901E3, pyunits.J/pyunits.kmol/pyunits.K**2),
'3': (8.125, pyunits.J/pyunits.kmol/pyunits.K**3),
'4': (-1.4116E-2, pyunits.J/pyunits.kmol/pyunits.K**4),
'5': (9.3701E-6, pyunits.J/pyunits.kmol/pyunits.K**5)},
"cp_mol_ig_comp_coeff": {
'A': (30.09200, pyunits.J/pyunits.mol/pyunits.K),
'B': (6.832514, pyunits.J*pyunits.mol**-1*pyunits.K**-1*pyunits.kiloK**-1),
'C': (6.793435, pyunits.J*pyunits.mol**-1*pyunits.K**-1*pyunits.kiloK**-2),
'D': (-2.534480, pyunits.J*pyunits.mol**-1*pyunits.K**-1*pyunits.kiloK**-3),
'E': (0.082139, pyunits.J*pyunits.mol**-1*pyunits.K**-1*pyunits.kiloK**2),
'F': (-250.8810, pyunits.kJ/pyunits.mol),
'G': (223.3967, pyunits.J/pyunits.mol/pyunits.K),
'H': (0, pyunits.kJ/pyunits.mol)},
"entr_mol_form_liq_comp_ref": (69.95, pyunits.J/pyunits.K/pyunits.mol)
# End parameter_data
}},
'H_+': {"type": Cation, "charge": 1,
# Define the methods used to calculate the following properties
"dens_mol_liq_comp": Constant,
"enth_mol_liq_comp": Constant,
"cp_mol_liq_comp": Constant,
"entr_mol_liq_comp": Constant,
# Parameter data is always associated with the methods defined above
"parameter_data": {
"mw": (1.00784, pyunits.g/pyunits.mol),
"dens_mol_liq_comp_coeff": (55, pyunits.kmol*pyunits.m**-3),
"enth_mol_form_liq_comp_ref": (0, pyunits.kJ/pyunits.mol),
"cp_mol_liq_comp_coeff": (75000, pyunits.J/pyunits.kmol/pyunits.K),
"entr_mol_form_liq_comp_ref": (0, pyunits.J/pyunits.K/pyunits.mol)
},
# End parameter_data
},
'OH_-': {"type": Anion, "charge": -1,
# Define the methods used to calculate the following properties
"dens_mol_liq_comp": Constant,
"enth_mol_liq_comp": Constant,
"cp_mol_liq_comp": Constant,
"entr_mol_liq_comp": Constant,
# Parameter data is always associated with the methods defined above
"parameter_data": {
"mw": (17.008, pyunits.g/pyunits.mol),
"dens_mol_liq_comp_coeff": (55, pyunits.kmol*pyunits.m**-3),
"enth_mol_form_liq_comp_ref": (-230.000, pyunits.kJ/pyunits.mol),
"cp_mol_liq_comp_coeff": (75000, pyunits.J/pyunits.kmol/pyunits.K),
"entr_mol_form_liq_comp_ref": (-10.75, pyunits.J/pyunits.K/pyunits.mol)
},
# End parameter_data
}
},
# End Component list
"phases": {'Liq': {"type": AqueousPhase,
"equation_of_state": Ideal},
},
"state_definition": FTPx,
# This is an optional dictionary to setup bounds on
# the state variables. Names below MUST correspond
# to the 'FTPx' type state definition
"state_bounds": {"flow_mol": (0, 50, 100),
"temperature": (273.15, 300, 650),
"pressure": (5e4, 1e5, 1e6)
},
# These are generally optional parameters, however, because we
# are using the Perry's model to calculate temperature dependent
# properties, we MUST provide these here.
"pressure_ref": 1e5,
"temperature_ref": 300,
# Our dictionary for base units MUST define the following
"base_units": {"time": pyunits.s,
"length": pyunits.m,
"mass": pyunits.kg,
"amount": pyunits.mol,
"temperature": pyunits.K},
}
# End thermo_config definition
There is a significant amount to breakdown here, so let’s discuss a couple of things step by step…
(1) All components need a "type"
. For this, you have a number of "type"
options within IDAES.
Generally, the "H2O"
component should always be a Solvent
within WaterTAP. Charged species
will always be either a Cation
or Anion
depending on the sign of their actual "charge"
.
More information on "components"
can be found at Components.
(2) All components need to have methods defined for calculating thermodynamic properties such as
"dens_mol_liq_comp"
, "cp_mol_liq_comp"
, "enth_mol_liq_comp"
, and "entr_mol_liq_comp"
.
In this example, we used the Perrys
method for "H2O"
and the Constant
method for
both of our ions. When we declare a specific method to calculate these properties, we are then
REQUIRED to include specific parameter information in the "parameter_data"
dictionary
defined within each component dictionary. For additional information regarding those parameter
needs, have a look at Perrys and Constant methods in IDAES.
(3) In this example, we are just setting up a configuration for water only. Thus, we are
not particularly interested in any other phases. In this case, we define the "phases"
dictionary to contain a single phase we named 'Liq'
and declared this to be an AqueousPhase
.
In WaterTAP, most of our models will be using AqueousPhase
, but may add additional phases
for effects such as precipitation and/or gas-absorbtion. Also, it should be noted that each phase
must also define a method for the "equation_of_state"
argument. In this case, we are assuming
that the phase behaves under the Ideal
assumption. For more information on phases and equations
of state, see Phases and EquationOfState.
(4) We chose to define the "state_definition"
as FTPx
, however, there are many more
options available. More information can be found in StateDefinition.
The reaction-properties configuration dictionary
If you did not include reactions in the thermo-properties dictionary (see How to use inherent reactions) and your system involves reactions, then you MUST also create and provide a reaction-properties configuration dictionary. Unlike the thermo-properties configuration dictionary, most of the keys within the reaction-properties dictionary are optional and depend on your system. The major keys to be aware of are as follows…
Key |
Description |
---|---|
base_units |
dictionary containing the base units the model uses (same as the thermo-properties) |
equilibrium_reactions |
dictionary containing the full set of equilibrium reactions in the system |
rate_reactions |
dictionary containing the full set of rate reactions in the system |
Note
Each type of reaction (equilibrium_reactions
and rate_reactions
) have
their own sets of parameters and methods to be declared. More information on
how to set up these arguments and the methods available can be found at
GenericReactions. You can go directly to either methods by following
the following links (EquilibriumReactions and RateReactions).
Example reaction-properties configuration dictionary
Following from our previous example for the thermo-properties configuration dictionary, here we will show how you setup a reaction-properties configuration dictionary for the dissociation of water. Since water dissociation is a fast acid reaction, we will model it as an equilibrium reaction.
# Importing the object for units from pyomo
from pyomo.environ import units as pyunits
# Import the object/function for heat of reaction
from idaes.models.properties.modular_properties.reactions.dh_rxn import constant_dh_rxn
# Import built-in Gibb's Energy function
from idaes.models.properties.modular_properties.reactions.equilibrium_constant import van_t_hoff
# Import safe log power law equation
from idaes.models.properties.modular_properties.reactions.equilibrium_forms import log_power_law_equil
# Importing the enum for concentration unit basis used in the 'get_concentration_term' function
from idaes.models.properties.modular_properties.base.generic_reaction import ConcentrationForm
reaction_config = {
"base_units": {"time": pyunits.s,
"length": pyunits.m,
"mass": pyunits.kg,
"amount": pyunits.mol,
"temperature": pyunits.K},
"equilibrium_reactions": {
"H2O_Kw": {
"stoichiometry": {("Liq", "H2O"): -1,
("Liq", "H_+"): 1,
("Liq", "OH_-"): 1},
"heat_of_reaction": constant_dh_rxn,
"equilibrium_constant": van_t_hoff,
"equilibrium_form": log_power_law_equil,
"concentration_form": ConcentrationForm.moleFraction,
"parameter_data": {
"dh_rxn_ref": (55.830, pyunits.kJ/pyunits.mol),
"k_eq_ref": (10**-14/55.2/55.2, pyunits.dimensionless),
"T_eq_ref": (298, pyunits.K),
# By default, reaction orders follow stoichiometry, so
# we manually set reaction order here to override.
# In our case, the water dissociation reaction is
# mathematically represented by Kw = [H_+]*[OH_-]
# thus, this reaction is of order 0 with respect
# to the [H2O] concentration.
"reaction_order": {("Liq", "H2O"): 0,
("Liq", "H_+"): 1,
("Liq", "OH_-"): 1}
}
# End parameter_data
}
# End reaction H2O_Kw
}
# End equilibrium_reactions
}
# End reaction_config definition
There is a significant amount of information and options available, so we will just go through some things of note here.
(1) Each reaction you add to the model will have its own dictionary with
essentially the same format as the "H2O_Kw"
dictionary shown above. Make sure
that each reaction you add has a unique key within the "equilibrium_reactions"
parent dictionary.
(2) The first thing you need to define about the reaction is the stoichiometry.
In IDAES, we follow the convention that products of a reaction should have
positive stoichiometric values and reactants of a reaction should have negative
stoichiometric values. This is true for both equilibrium_reactions
and
rate_reactions
.
(3) The "stoichiometry"
dictionary under the reaction has tuple keys. In
this format, the first item in the tuple is the phase
of the species involved in
the reaction and the second item in the tuple is the name
of the species. Recall
that in the thermo-properties configuration dictionary, we named the AqueousPhase
as "Liq"
, thus we must reference that same name here in the reaction-properties
configuration dictionary. The specific species must also be referenced by the names
they were given in the thermo-properties configuration dictionary.
(4) You must provide methods/options for each of the following: "heat_of_reaction"
,
"equilibrium_constant"
, "equilibrium_form"
, and "concentration_form"
. These
methods will define how IDAES computes the heat of reaction in the energy balance, the
equilibrium constant or K value for this reaction constraint, the mathematical representation
of the equilibrium constraint, and what the concentration form is for the species involved
in this reaction, respectively. Many options are available for all of these and more
information on each can be found at ReactionMethods and ConcentrationForm.
(5) The "parameter_data"
dictionary must contain the parameter information
required by the chosen methods from (4) above. See ReactionMethods for more
details.
(6) Within the "parameter_data"
dictionary is an optional dictionary for
"reaction_order"
. If this dictionary is not provided, then it is assumed that
the order of the reaction form with respect to each species just follows the
"stoichiometry"
dictionary from above. However, in certain cases you may need
to override that assumption. In this particular case, we override the reaction
order to zero out the order with respect to the water concentration. This is
standard practice for aqueous acid-base chemistry.
Note
The "reaction_order"
dictionary follows the same sign convention for products
and reactants as the "stoichiometry"
dictionary. Positive signs for products
and negative signs for reactants.
Defining a unit model
Once you have your thermo-properties and (optionally) your reaction-properties configuration dictionaries setup, you will want to put them into a unit model so that you can simulate that particular unit process with the chemistry you have specified. Within IDAES, their are numerous unit models to chose from that will support the inclusion of these chemistry configurations. A list of the unit models available, and how to use them, are provided here (UnitModels).
In this guide, we will not cover all the unit models, but will give one basic example of how to use the configuration dictionaries defined above with the EquilibriumReactor model.
Example: Using our configuration dictionaries in an EquilibriumReactor
Recall, we had named our configuration dictionaries as thermo_config
and
reaction_config
. We will reference those dictionary names in the example
code below.
# Import specific pyomo objects
from pyomo.environ import ConcreteModel
# Import the core idaes objects for Flowsheets and types of balances
from idaes.core import FlowsheetBlock
# Import the idaes objects for Generic Properties and Reactions
from idaes.models.properties.modular_properties.base.generic_property import GenericParameterBlock
from idaes.models.properties.modular_properties.base.generic_reaction import GenericReactionParameterBlock
# Import the idaes object for the EquilibriumReactor unit model
from idaes.models.unit_models.equilibrium_reactor import EquilibriumReactor
# Create an instance of a pyomo model
model = ConcreteModel()
# Add an IDAES flowsheet to that model
model.fs = FlowsheetBlock(dynamic=False)
# Add a thermo parameter block to that flowsheet
# Here, we are passing our 'thermo_config' dictionary we created earlier
model.fs.thermo_params = GenericParameterBlock(**thermo_config)
# Add a reaction parameter block to that flowsheet
# Here, we are passing our thermo block created above as the property package
# and then giving our 'reaction_config' as the instructions for how the
# reactions will be constructed from the thermo package.
model.fs.rxn_params = GenericReactionParameterBlock(property_package=model.fs.thermo_params, **reaction_config)
# Add an EquilibriumReactor object as the unit model
# Here, we pass both the thermo package and reaction package, as well
# as a number of other arguments to help define how this unit process
# will behave.
model.fs.unit = EquilibriumReactor(property_package=model.fs.thermo_params,
reaction_package=model.fs.rxn_params,
has_rate_reactions=False,
has_equilibrium_reactions=True,
has_heat_transfer=False,
has_heat_of_reaction=False,
has_pressure_change=False)
# At this point, you can 'fix' your inlet/outlet state conditions,
# setup scaling factors, initialize the model, then solve the model
# just as you would with any other IDAES flowsheet
In the example code above, we show how to setup the thermo and reaction packages and place them into the EquilibriumReactor unit model, but do not go further. Additional instructions for setting up and solving unit models can be found at IDAESWorkflow.