##############################################################################
# Institute for the Design of Advanced Energy Systems Process Systems
# Engineering Framework (IDAES PSE Framework) Copyright (c) 2018, by the
# software owners: The Regents of the University of California, through
# Lawrence Berkeley National Laboratory, National Technology & Engineering
# Solutions of Sandia, LLC, Carnegie Mellon University, West Virginia
# University Research Corporation, et al. All rights reserved.
#
# Please see the files COPYRIGHT.txt and LICENSE.txt for full copyright and
# license information, respectively. Both files are also available online
# at the URL "https://github.com/IDAES/idaes".
##############################################################################
"""
Standard IDAES flash model.
"""
from __future__ import division
# Import Python libraries
import logging
# Import Pyomo libraries
from pyomo.environ import Expression, Set
from pyomo.network import Port
from pyomo.common.config import ConfigValue, In
# Import IDAES cores
from idaes.core import UnitBlockData, declare_process_block_class, \
Holdup0D, CONFIG_Base
from idaes.core.util.config import is_parameter_block, list_of_strings
from idaes.core.util.misc import add_object_ref
__author__ = "Andrew Lee"
# Set up logger
logger = logging.getLogger('idaes.unit_model')
[docs]@declare_process_block_class("Flash")
class FlashData(UnitBlockData):
"""
Standard Flash Unit Model Class
"""
CONFIG = CONFIG_Base()
# Set default values of inherited attributes
CONFIG.get("has_phase_equilibrium")._default = True
CONFIG.get("has_heat_transfer")._default = True
CONFIG.get("has_pressure_change")._default = True
# Add unit model attributes
CONFIG.declare("property_package", ConfigValue(
default=None,
domain=is_parameter_block,
description="Property package to use for holdup",
doc="""Property parameter object used to define property calculations
(default = 'use_parent_value')
- 'use_parent_value' - get package from parent (default = None)
- a ParameterBlock object"""))
CONFIG.declare("property_package_args", ConfigValue(
default={},
description="Arguments to use for constructing property packages",
doc="""A dict of arguments to be passed to the PropertyBlockData
and used when constructing these
(default = 'use_parent_value')
- 'use_parent_value' - get package from parent (default = None)
- a dict (see property package for documentation)"""))
CONFIG.declare("inlet_list", ConfigValue(
domain=list_of_strings,
description="List of inlet names",
doc="""A list containing names of inlets (default = None)
- None - default single inlet
- list - a list of names for inlets"""))
CONFIG.declare("num_inlets", ConfigValue(
domain=int,
description="Number of inlets to unit",
doc="""Argument indicating number (int) of inlets to construct
(default = None). Not used if inlet_list arg is provided.
- None - use inlet_list arg instead
- int - Inlets will be named with sequential numbers from 1
to num_inlets"""))
CONFIG.declare("outlet_list", ConfigValue(
default=["Vap", "Liq"],
domain=list_of_strings,
description="List of outlet names",
doc="""A list containing names of outlets (default = None)
- None - default single outlet
- list - a list of names for outlets"""))
CONFIG.declare("num_outlets", ConfigValue(
domain=int,
description="Number of outlets to unit",
doc="""Argument indicating number (int) of outlets to construct
(default = None). Not used if outlet_list arg is provided.
- None - use outlet_list arg instead
- int - Outlets will be named with sequential numbers from 1
to num_outlets"""))
CONFIG.declare("outlet_type", ConfigValue(
default='generic',
domain=In(['generic', 'direct']),
description="Outlet splitter type",
doc="""Argument indicating whether to use a generic OutletSplitter
block (default) or a direct link to the properties_out block."""))
CONFIG.declare("energy_split_type", ConfigValue(
default='temperature',
domain=In(['temperature', 'enth_mol', 'enth_mass', 'energy_balance']),
description="Energy splitting method",
doc="""Argument indiciating method to use when splitting material
flows between outlets, **default** - 'temperature'. **Valid values:** {
**'temperature'** - equate temperatures in outlet streams, **'enth_mol'** -
equate specific molar enthalpies in each outlet, **'enth_mass'** - equate
specific mass enthalpies in each outlet, **'energy_balance'** - split using an
energy balance with a split fraction.}"""))
[docs] def build(self):
"""
Begin building model (pre-DAE transformation).
Args:
None
Returns:
None
"""
# Call UnitModel.build to setup dynamics
super(FlashData, self).build()
# Build Holdup Block
self.holdup = Holdup0D()
# Set Unit Geometry and holdup Volume
self._set_geometry()
[docs] def post_transform_build(self):
"""
Continue model construction after DAE transformation.
Args:
None
Returns:
None
"""
# Construct Inlets
self.build_inlets(inlets=self.config.inlet_list,
num_inlets=self.config.num_inlets)
# Build Outlets
if self.config.outlet_type == "direct":
self._build_direct_outlet()
else:
self._build_generic_outlet()
# Construct performance equations
self._make_performance()
def _set_geometry(self):
"""
Define the geometry of the unit as necessary, and link to holdup volume
Args:
None
Returns:
None
"""
# For this case, just create a reference to holdup volume
if self.config.include_holdup is True:
add_object_ref(self, "volume", self.holdup.volume)
def _build_generic_outlet(self):
"""
Build a generic OutletSplitter block
Args:
None
Returns:
None
"""
self.build_outlets(outlets=self.config.outlet_list,
num_outlets=self.config.num_outlets,
material_split_type="phase",
energy_split_type=self.config.energy_split_type)
def _build_direct_outlet(self):
"""
Build outlets with direct links to properties_out block.
Args:
None
Returns:
None
"""
def _state_error(self):
# Method to log a warning and construct OutletSplitter if
# state variables do not appear to be suitable for direct splitting
logger.warning("{} property package does not appear to "
"use a set of state variables suitable for direct "
"outlet creation. An OutletSplitter "
"block will be created instead. Please refer "
"to the documentation for more details."
.format(self.name))
self.config.outlet_type = 'generic'
self._build_generic_outlet()
return
def _prop_error(self):
# Method to log a warning and construct OutletSplitter if prop pack
# does not support properties necessary for direct splitting
logger.warning("{} property package does not support properties "
"necessary for direct outlet creation. An "
"OutletSplitter block will be created instead. "
"Please refer to the documentation for more "
"details.".format(self.name))
self.config.outlet_type = 'generic'
self._build_generic_outlet()
return
# Get list of state variables and supported properties from prop pack
svars = self.holdup.properties_out[0].declare_port_members()
sprops = self.config.property_package.get_supported_properties()
# Check that all state vars are recognised
if not all(k in ["flow_mol", "flow_mol_phase", "flow_mol_comp",
"flow_mol_phase_comp", "mole_frac", "mole_frac_phase",
"flow_mass", "flow_mass_phase", "flow_mass_comp",
"flow_mass_phase_comp", "mass_frac",
"mass_frac_phase",
"temperature", "enth_mol", "enth_mass",
"enth_mol_phase", "enth_mass_phase",
"pressure"]
for k in svars):
return _state_error(self)
# Check that Property Package uses a recognised set of state vars and
# supports the necessary property components
pcount = 0
for b in ['mol', 'mass']:
if (("flow_"+b in svars) and
((b == "mol" and "mole_frac" in svars) or
("mass_frac" in svars))):
stype = ('', b)
fb = 'mole' if b == 'mol' else 'mass'
if not all(k in sprops for k in ("flow_"+b+"_phase",
fb+"_frac_phase")):
return _prop_error(self)
break
elif "flow_"+b+"_comp" in svars:
stype = ('_comp', b)
if "flow_"+b+"_phase_comp" not in sprops:
return _prop_error(self)
break
elif "flow_"+b+"_phase_comp" in svars:
stype = ('_phase_comp', b)
if "flow_"+b+"_phase_comp" not in sprops:
return _prop_error(self)
break
elif (("flow_"+b+"_phase" in svars) and
((b == "mol" and "mole_frac_phase" in svars) or
("mass_frac_phase" in svars))):
stype = ('_phase', b)
fb = 'mole' if b == 'mol' else 'mass'
if not all(k in sprops for k in ("flow_"+b+"_phase",
fb+"_frac_phase")):
return _prop_error(self)
break
else:
pcount = pcount+1
if pcount > 1:
return _state_error(self)
if 'enth_mol' in svars or 'enth_mol_phase' in svars:
if 'enth_mol_phase' not in sprops:
return _prop_error(self)
if 'enth_mass' in svars or 'enth_mass_phase' in svars:
if 'enth_mass_phase' not in sprops:
return _prop_error(self)
elif 'temperature' in svars:
if 'temperature' not in sprops:
return _prop_error(self)
# Check outlet construction arguments and raise warning if not defaults
if ((self.config.outlet_list != ['Vap', 'Liq']) and (
self.config.num_outlets is not None)):
logger.warning("{} outlet_list and/or num_oulets arguments have "
"been provided whilst using outlet_type = 'direct'."
" When using direct outlets, only two outlets will "
"be created, and other arguments will be ignored"
.format(self.name))
self.config.outlet_list = ['Vap', 'Liq']
self.config.num_outlets = None
# Create outlet object
self.outlet_idx = Set(initialize=self.config.outlet_list)
self.outlet = Port(self.time,
self.outlet_idx,
noruleinit=True,
doc="Outlet port object")
# Populate outlet object
for t in self.time:
if "pressure" in svars:
self.outlet[t, "Vap"].add(
self.holdup.properties_out[t].pressure,
"pressure")
self.outlet[t, "Liq"].add(
self.holdup.properties_out[t].pressure,
"pressure")
if "temperature" in svars:
self.outlet[t, "Vap"].add(
self.holdup.properties_out[t].temperature,
"temperature")
self.outlet[t, "Liq"].add(
self.holdup.properties_out[t].temperature,
"temperature")
if "enth_mol" in svars:
self.outlet[t, "Vap"].add(
self.holdup.properties_out[t].enth_mol_phase['Vap'],
"enth_mol")
self.outlet[t, "Liq"].add(
self.holdup.properties_out[t].enth_mol_phase['Liq'],
"enth_mol")
if "enth_mass" in svars:
self.outlet[t, "Vap"].add(
self.holdup.properties_out[t].enth_mass_phase['Vap'],
"enth_mass")
self.outlet[t, "Liq"].add(
self.holdup.properties_out[t].enth_mass_phase['Liq'],
"enth_mass")
if "enth_mol_phase" in svars:
self.outlet[t, "Vap"].add(
self.holdup.properties_out[t].enth_mol_phase,
"enth_mol_phase")
self.outlet[t, "Liq"].add(
self.holdup.properties_out[t].enth_mol_phase,
"enth_mol_phase")
if "enth_mass_phase" in svars:
self.outlet[t, "Vap"].add(
self.holdup.properties_out[t].enth_mass_phase,
"enth_mass_phase")
self.outlet[t, "Liq"].add(
self.holdup.properties_out[t].enth_mass_phase,
"enth_mass_phase")
# If using component based flows, add only a flow term
if stype[0] in ["_phase_comp", "_comp"]:
vobj = getattr(self.holdup.properties_out[t],
"flow_"+stype[1]+"_phase_comp")
# Create Expressions in Property Blocks to hold variables
if stype[0] == "_comp":
def rule_vap(b, j):
return vobj["Vap", j]
self.holdup.properties_out[t]._vapor_outlet_flow = (
Expression(
self.holdup.properties_out[t].component_list,
rule=rule_vap))
def rule_liq(b, j):
return vobj["Liq", j]
self.holdup.properties_out[t]._liquid_outlet_flow = (
Expression(
self.holdup.properties_out[t].component_list,
rule=rule_liq))
else:
def rule_vap(b, p, j):
if p == "Vap":
return vobj["Vap", j]
else:
return 0
self.holdup.properties_out[t]._vapor_outlet_flow = (
Expression(
self.holdup.properties_out[t].phase_list,
self.holdup.properties_out[t].component_list,
rule=rule_vap))
def rule_liq(b, p, j):
if p == "Liq":
return vobj["Liq", j]
else:
return 0
self.holdup.properties_out[t]._liquid_outlet_flow = (
Expression(
self.holdup.properties_out[t].phase_list,
self.holdup.properties_out[t].component_list,
rule=rule_liq))
# Add Expressions to Port
self.outlet[t, "Vap"].add(
self.holdup.properties_out[t]._vapor_outlet_flow,
"flow_"+stype[1]+stype[0])
self.outlet[t, "Liq"].add(
self.holdup.properties_out[t]._liquid_outlet_flow,
"flow_"+stype[1]+stype[0])
# Otherwise, need both flow and mass/mole fraction
else:
vobj = getattr(self.holdup.properties_out[t],
"flow_"+stype[1]+"_phase")
fb = 'mole' if stype[1] == 'mol' else 'mass'
fobj = getattr(self.holdup.properties_out[t],
fb+"_frac_phase")
# Create Expressions in Property Blocks to hold variables
if stype[0] == "_phase":
def rule_vap_flow(b, p):
if p == "Vap":
return vobj["Vap"]
else:
return 0
self.holdup.properties_out[t]._vapor_outlet_flow = (
Expression(
self.holdup.properties_out[t].phase_list,
rule=rule_vap_flow))
def rule_liq_flow(b, p):
if p == "Liq":
return vobj["Liq"]
else:
return 0
self.holdup.properties_out[t]._liquid_outlet_flow = (
Expression(
self.holdup.properties_out[t].phase_list,
rule=rule_liq_flow))
def rule_vap_frac(b, p, j):
if p == "Vap":
return fobj["Vap", j]
else:
return 0
self.holdup.properties_out[t]._vapor_outlet_frac = (
Expression(
self.holdup.properties_out[t].phase_list,
self.holdup.properties_out[t].component_list,
rule=rule_vap_frac))
def rule_liq_frac(b, p, j):
if p == "Liq":
return fobj["Liq", j]
else:
return 0
self.holdup.properties_out[t]._liquid_outlet_frac = (
Expression(
self.holdup.properties_out[t].phase_list,
self.holdup.properties_out[t].component_list,
rule=rule_liq_frac))
else:
def rule_vap_flow(b):
return vobj["Vap"]
self.holdup.properties_out[t]._vapor_outlet_flow = (
Expression(rule=rule_vap_flow))
def rule_liq_flow(b):
return vobj["Liq"]
self.holdup.properties_out[t]._liquid_outlet_flow = (
Expression(rule=rule_liq_flow))
def rule_vap_frac(b, j):
return fobj["Vap", j]
self.holdup.properties_out[t]._vapor_outlet_frac = (
Expression(
self.holdup.properties_out[t].component_list,
rule=rule_vap_frac))
def rule_liq_frac(b, j):
return fobj["Liq", j]
self.holdup.properties_out[t]._liquid_outlet_frac = (
Expression(
self.holdup.properties_out[t].component_list,
rule=rule_liq_frac))
# Add Expressions to Port
self.outlet[t, "Vap"].add(
self.holdup.properties_out[t]._vapor_outlet_flow,
"flow_"+stype[1]+stype[0])
self.outlet[t, "Liq"].add(
self.holdup.properties_out[t]._liquid_outlet_flow,
"flow_"+stype[1]+stype[0])
self.outlet[t, "Vap"].add(
self.holdup.properties_out[t]._vapor_outlet_frac,
fb+"_frac"+stype[0])
self.outlet[t, "Liq"].add(
self.holdup.properties_out[t]._liquid_outlet_frac,
fb+"_frac"+stype[0])
def _make_performance(self):
"""
Define constraints which describe the behaviour of the unit model.
Args:
None
Returns:
None
"""
# Set split fractions
ol = self.config.outlet_list
if (ol is not None and "Vap" in ol and "Liq" in ol and len(ol) == 2
and self.config.outlet_type != 'direct'):
for t in self.time:
self.holdup.outlet_splitter.split_fraction[t,
"Vap",
"Vap"].fix(1.0)
self.holdup.outlet_splitter.split_fraction[t,
"Liq",
"Liq"].fix(1.0)
# Set references to balance terms at unit level
if (self.config.has_heat_transfer is True and
self.config.energy_balance_type != 'none'):
add_object_ref(self, "heat", self.holdup.heat)
if (self.config.has_pressure_change is True and
self.config.momentum_balance_type != 'none'):
add_object_ref(self, "deltaP", self.holdup.deltaP)