##############################################################################
# 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".
##############################################################################
"""
These classes handle the mixing and splitting of multiple inlets/outlets to
a single holdup block.
"""
from __future__ import division
import logging
from pyomo.environ import (Constraint, Param, PositiveReals,
Set, SolverFactory, Var)
from pyomo.common.config import ConfigValue, In
from idaes.core import ProcessBlockData, declare_process_block_class
from idaes.core.util.config import 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]class Port(ProcessBlockData):
"""
Base Port Class
This class contains methods common to all Port classes.
"""
CONFIG = ProcessBlockData.CONFIG()
CONFIG.declare("has_material_balance", ConfigValue(
default=True,
domain=In([True, False]),
description="Material mixing construction flag",
doc="""Indicates whether material mixing constraints should be
constructed."""))
CONFIG.declare("has_energy_balance", ConfigValue(
default=True,
domain=In([True, False]),
description="Energy mixing construction flag",
doc="""Indicates whether energy mixing constraints should be
constructed."""))
CONFIG.declare("has_momentum_balance", ConfigValue(
default=True,
domain=In([True, False]),
description="Momentum mixing construction flag",
doc="""Indicates whether momentum mixing constraints should be
constructed."""))
[docs] def build(self):
"""
General build method for Ports. This method calls a number
of methods common to all Port blocks.
Inheriting models should call `super().build`.
Args:
None
Returns:
None
"""
# Perform build tasks common to all Ports
self._make_common_references()
self._get_build_flags()
def _make_common_references(self):
"""
Construct references to component in the parent Holdup block that are
need within Ports.
Args:
None
Returns:
None
"""
# Make references to time, and component and phase lists
add_object_ref(self, "time", self.parent_block().time)
add_object_ref(self, 'component_list',
self.parent_block().component_list)
add_object_ref(self, 'phase_list',
self.parent_block().phase_list)
def _get_build_flags(self):
"""
Collect construction flags from parent Holdup block to determine which
balance equations need to be constructed.
Args:
None
Returns:
None
"""
# Check the parent holdup block for build flags
if self.parent_block().config.material_balance_type == 'none':
self.config.has_material_balance = False
else:
self.config.has_material_balance = True
if self.parent_block().config.energy_balance_type == 'none':
self.config.has_energy_balance = False
else:
self.config.has_energy_balance = True
if self.parent_block().config.momentum_balance_type == 'none':
self.config.has_momentum_balance = False
else:
self.config.has_momentum_balance = True
[docs]@declare_process_block_class('InletMixer')
class InletMixerData(Port):
"""
Inlet Mixer Class
This class builds a mixer to allow for multiple inlets to a single holdup
block. The class constructs property blocks for each inlet and creates
mixing rules to connect them to the property block within the associated
holdup block.
"""
# Create Class ConfigBlock
CONFIG = Port.CONFIG()
CONFIG.declare("inlets", ConfigValue(
default=None,
domain=list_of_strings,
description="List of inlets",
doc="""A list of strings to be used to name inlet streams."""))
[docs] def build(self):
"""
Build method for Mixer blocks. This method calls a number of methods
to construct the necessary balance equations for the Mixer.
Args:
None
Returns:
None
"""
if self.config.inlets is None:
raise ValueError("{} inlets argument not assigned. A list of "
"inlet names must be provided.".format(self.name))
# Call UnitModel.build to setup dynamics
super(InletMixerData, self).build()
# Get references to holdup inlet properties
self._make_references()
# Build property blocks
self._build_property_blocks()
# Build mixing rules
if self.config.has_material_balance:
self._mix_material()
if self.config.has_energy_balance:
self._mix_energy()
if self.config.has_momentum_balance:
self._mix_pressure()
def _make_references(self):
"""
Make a local reference to the inlet property block (or element of an
indexed Property Block) to use in balance equations.
Args:
None
Returns:
None
"""
# Make references to holdup inlet properties
try:
# Guess Holdup0D
add_object_ref(self, 'properties_in',
self.parent_block().properties_in)
except AttributeError:
try:
self.properties_in = {}
for t in self.time:
if hasattr(self.parent_block(), 'ldomain'):
# Assume Holdup1D and check for flow direction
if (self.parent_block().config.flow_direction
is "forward"):
prop_keys = (t,
self.parent_block().ldomain.first())
else:
prop_keys = (t, self.parent_block().ldomain.last())
else:
prop_keys = t
self.properties_in[t] = \
self.parent_block().properties[prop_keys]
except AttributeError:
raise ValueError('{} Unrecognised type of holdup block. '
'InletMixer only supports Holdup0D, '
'Holdup1D or HoldupStatic.'.format(self.name))
def _build_property_blocks(self):
"""
Construct Property Blocks for each inlet stream.
Args:
None
Returns:
None
"""
# Creat inlex_idx as Pyomo Set
self.inlet_idx = Set(initialize=self.config.inlets, ordered=True)
# Create property blocks
self.properties = \
self.parent_block().property_module.PropertyBlock(
self.time,
self.inlet_idx,
doc='Property blocks for inlets',
has_sum_fractions=False,
calculate_equilibrium_reactions=False,
calculate_phase_equilibrium=False,
parameters=self.parent_block().config.property_package,
**self.parent_block().config.property_package_args)
def _mix_material(self):
"""
Construct material mixing constraints.
Args:
None
Returns:
None
"""
# Creating material mixing constraint
@self.Constraint(self.time,
self.phase_list,
self.component_list,
doc='Material mixing constraints')
def material_mixing(b, t, p, j):
return self.properties_in[t].material_balance_term[p, j] == (
sum(self.properties[t, i].material_balance_term[p, j]
for i in self.inlet_idx))
# TODO : add support for other types of energy balance? e.g. by phase.
def _mix_energy(self):
"""
Construct energy mixing constraint.
Args:
None
Returns:
None
"""
self.scaling_factor_energy = Param(
default=1e-6,
mutable=True,
doc='Energy balance scaling parameter')
# Creating energy mixing constraint
sf = self.scaling_factor_energy
@self.Constraint(self.time,
doc='Energy mixing constraint')
def energy_mixing(b, t):
return sf*sum(b.properties_in[t].energy_balance_term[p]
for p in b.phase_list) == sf*(
sum(sum(b.properties[t, i].energy_balance_term[p]
for i in b.inlet_idx) for p in b.phase_list))
def _mix_pressure(self):
"""
Construct pressure mixing constraints. These constraints use a smooth
minimum function to determine the minimum pressure amongst all inlet
streams, and sets the pressure of the mixed stream to this value.
Args:
None
Returns:
None
"""
# Calculate minimum inlet pressure
self.minimum_pressure = Var(self.time,
self.inlet_idx,
doc='Variable for calculating '
'minimum inlet pressure')
self.eps_pressure = Param(mutable=True,
initialize=1e-3,
domain=PositiveReals,
doc='Smoothing term for '
'minimum inlet pressure')
self.scaling_factor_pressure = Param(
default=1e-5,
mutable=True,
doc='Pressure scaling parameter')
# Calculate minimum inlet pressure
sf = self.scaling_factor_pressure
@self.Constraint(self.time,
self.inlet_idx,
doc='Calculation for minimum inlet pressure')
def minimum_pressure_constraint(b, t, i):
if i == self.inlet_idx.first():
return sf*self.minimum_pressure[t, i] == sf*(
self.properties[t, i].pressure)
else:
return sf*self.minimum_pressure[t, i] == sf*(
0.5*(self.minimum_pressure[t, self.inlet_idx.prev(i)] +
self.properties[t, i].pressure -
((self.minimum_pressure[t, self.inlet_idx.prev(i)] -
self.properties[t, i].pressure +
self.eps_pressure)**2)**0.5))
# Set inlet pressure to minimum pressure
@self.Constraint(self.time, doc='link pressure to holdup')
def inlet_pressure(b, t):
return self.properties_in[t].pressure == (
self.minimum_pressure[t,
self.inlet_idx.last()])
[docs] def model_check(blk):
"""
Calls model checks on all associated Property Blocks.
Args:
None
Returns:
None
"""
# Try property block model check
try:
blk.parent_block().property_module.model_check(blk.properties)
except AttributeError:
logger.warning('{} Property package has no model check. To correct'
' this, add a model_check method to the associated'
' PropertyBlock class.'.format(blk.name))
[docs] def initialize(blk, state_args=None, outlvl=0, optarg=None,
solver='ipopt', hold_state=True):
''' Initialisation routine for InletMixer (default solver ipopt)
Keyword Arguments:
state_args : a dict of arguments to be passed to the property
package(s) to provide an initial state for
initialization (see documentation of the specific
property package) (default = {}).
outlvl : sets output level of initialisation routine
* 0 = no output (default)
* 1 = return solver state for each step in routine
* 2 = include solver output infomation (tee=True)
optarg : solver options dictionary object (default=None)
solver : str indicating whcih solver to use during
initialization (default = 'ipopt')
hold_state : flag indicating whether the initialization routine
should unfix any state variables fixed during
initialization (default=True).
* True = state varaibles are not unfixed, and
a dict of returned containing flags for
which states were fixed during
initialization.
* False = state variables are unfixed after
initialization by calling the
relase_state method
Returns:
If hold_states is True, returns a dict containing flags for
which states were fixed during initialization.
'''
# Set solver options
opt = SolverFactory(solver)
opt.options = optarg
# Check state arguments
state_args = {} if state_args is None else state_args
for t in blk.time:
blk.properties_in[t].pressure =\
min(blk.properties[t, i].pressure.value for i in blk.inlet_idx)
for i in blk.inlet_idx:
if i == blk.inlet_idx.first():
blk.minimum_pressure[t, i].value =\
blk.properties[t, i].pressure.value
else:
blk.minimum_pressure[t, i].value = min(
blk.minimum_pressure[t, blk.inlet_idx.prev(i)].value,
blk.properties[t, i].pressure.value)
# Initialize property blocks
flags = blk.properties.initialize(outlvl=outlvl-1,
optarg=optarg,
solver=solver,
hold_state=hold_state,
**state_args)
if outlvl > 0:
logger.info('{} Initialisation Complete'.format(blk.name))
if hold_state:
return flags
else:
blk.release_state(flags, outlvl)
[docs] def release_state(blk, flags, outlvl=0):
'''
Method to relase state variables fixed during initialisation.
Keyword Arguments:
flags : dict containing information of which state variables
were fixed during initialization, and should now be
unfixed. This dict is returned by initialize if
hold_state=True.
outlvl : sets output level of of logging (default=0)
Returns:
None
'''
blk.properties.release_state(flags, outlvl=outlvl-1)
[docs]@declare_process_block_class('OutletSplitter')
class OutletSplitterData(Port):
"""
Outlet Mixer Class
This class builds a splitter to allow for multiple outlets to a single
holdup block. The class constructs property blocks for each outlet and
creates splitting rules to connect them to the property block within the
associated holdup block.
"""
# Create Class ConfigBlock
CONFIG = Port.CONFIG()
CONFIG.declare("outlets", ConfigValue(
default=None,
domain=list_of_strings,
description="List of outlets",
doc="""A list of strings to be used to name outlet streams."""))
CONFIG.declare("split_type", ConfigValue(
default='flow',
domain=In(['flow', 'phase', 'component', 'total']),
description="List of outlets",
doc="""A list of strings to be used to name outlet streams."""))
[docs] def build(self):
"""
Build method for Splitter blocks. This method calls a number of methods
to construct the necessary balance equations for the Splitter.
Args:
None
Returns:
None
"""
# Check that outlet argument was assigned
if self.config.outlets is None:
raise ValueError("{} outlets argument not assigned. A list of "
"outlet names must be provided."
.format(self.name))
# Call UnitModel.build to setup dynamics
super(OutletSplitterData, self).build()
# Get references to holdup outlet properties
self._make_references()
# Build property blocks
self._build_property_blocks()
# Make split fraction
self._make_split_fraction()
# Build mixing rules
if self.config.has_material_balance:
self._split_material()
if self.config.has_energy_balance:
self._split_energy()
if self.config.has_momentum_balance:
self._split_pressure()
def _make_references(self):
"""
Make a local reference to the outlet property block (or element of an
indexed Property Block) to use in balance equations.
Args:
None
Returns:
None
"""
# Make references to holdup outlet properties
try:
# Guess Holdup0D
add_object_ref(self, 'properties_out',
self.parent_block().properties_out)
except AttributeError:
try:
self.properties_out = {}
for t in self.time:
if hasattr(self.parent_block(), 'ldomain'):
# Assume Holdup1D and check for flow direction
if (self.parent_block().config.flow_direction
is "forward"):
prop_keys = (t, self.parent_block().ldomain.last())
else:
prop_keys = (t,
self.parent_block().ldomain.first())
else:
prop_keys = t
self.properties_out[t] = \
self.parent_block().properties[prop_keys]
except AttributeError:
raise ValueError('{} Unrecognised type of holdup block. '
'OutletSplitter only supports Holdup0D, '
'Holdup1D or HoldupStatic.'.format(self.name))
def _build_property_blocks(self):
"""
Construct Property Blocks for each outlet stream.
Args:
None
Returns:
None
"""
# Creat inlex_idx as Pyomo Set
self.outlet_idx = Set(initialize=self.config.outlets, ordered=True)
# TODO:should we allow outlet property blocks to calculate equilibrium?
# Create property blocks
self.properties = \
self.parent_block().property_module.PropertyBlock(
self.time,
self.outlet_idx,
doc='Property blocks for outlets',
has_sum_fractions=True,
calculate_equilibrium_reactions=False,
calculate_phase_equilibrium=False,
parameters=self.parent_block().config.property_package,
**self.parent_block().config.property_package_args)
def _make_split_fraction(self):
"""
Construct split fraction and associated constraints for Splitter.
Args:
None
Returns:
None
"""
# Check split_type to determine indexes on split_fraction
if self.config.split_type == 'flow':
split_args = [self.time, self.outlet_idx]
elif self.config.split_type == 'phase':
split_args = [self.time, self.outlet_idx, self.phase_list]
elif self.config.split_type == 'component':
split_args = [self.time, self.outlet_idx, self.component_list]
else:
split_args = [self.time, self.outlet_idx,
self.phase_list, self.component_list]
# Create split_fraction variable
self.split_fraction = Var(*split_args,
doc='Outlet split fraction')
# Add constraint on sum of split_fraction
if self.config.split_type == 'flow':
@self.Constraint(self.time,
doc='Sum of split_fractions must be 1')
def sum_split_fractions(b, t):
return 1 == sum(b.split_fraction[t, o]
for o in self.outlet_idx)
elif self.config.split_type == 'phase':
@self.Constraint(self.time,
self.phase_list,
doc='Sum of split_fractions must be 1')
def sum_split_fractions(b, t, p):
return 1 == sum(b.split_fraction[t, o, p]
for o in self.outlet_idx)
elif self.config.split_type == 'component':
@self.Constraint(self.time,
self.component_list,
doc='Sum of split_fractions must be 1')
def sum_split_fractions(b, t, j):
return 1 == sum(b.split_fraction[t, o, j]
for o in self.outlet_idx)
else:
@self.Constraint(self.time,
self.phase_list,
self.component_list,
doc='Sum of split_fractions must be 1')
def sum_split_fractions(b, t, p, j):
return 1 == sum(b.split_fraction[t, o, p, j]
for o in self.outlet_idx)
def _split_material(self):
"""
Construct material splitting constraints.
Args:
None
Returns:
None
"""
# Create substitution method for split_fraction
def split_sub(b, t, o, p, j):
if self.config.split_type == 'flow':
return b.split_fraction[t, o]
elif self.config.split_type == 'phase':
return b.split_fraction[t, o, p]
elif self.config.split_type == 'component':
return b.split_fraction[t, o, j]
else:
return b.split_fraction[t, o, p, j]
# Get phase component list(s)
if hasattr(self.parent_block().config.property_package,
"phase_component_list"):
phase_component_list = (
self.parent_block()
.config.property_package.phase_component_list)
else:
# Otherwise assume all components in all phases
phase_component_list = {}
for p in self.phase_list:
phase_component_list[p] = self.component_list
# Creating material splitting constraint
@self.Constraint(self.time,
self.outlet_idx,
self.phase_list,
self.component_list,
doc='Material splitting constraints')
def material_splitting(b, t, o, p, j):
if j in phase_component_list[p]:
return self.properties[t, o].material_balance_term[p, j] == (
self.properties_out[t].material_balance_term[p, j] *
split_sub(b, t, o, p, j))
else:
return Constraint.Skip
def _split_energy(self):
"""
Construct energy splitting constraints.
Args:
None
Returns:
None
"""
# Check split type to determine how to split energy
if self.config.split_type in ['phase']:
# Split enthalpy by split_fraction
# Create substitution method for split_fraction
def split_sub(b, t, o, p):
return b.split_fraction[t, o, p]
# TODO: specifying individual phase splits overspecifies problem.
# Need to consider implications for more complex energy balances
# Creating enthalpy splitting constraint
@self.Constraint(self.time,
self.outlet_idx,
doc='Enthalpy splitting constraints')
def energy_splitting(b, t, o):
return sum(self.properties[t, o].energy_balance_term[p]
for p in self.phase_list) == (
sum(self.properties_out[t].energy_balance_term[p] *
split_sub(b, t, o, p) for p in self.phase_list))
else:
# Equate temperatures
@self.Constraint(self.time,
self.outlet_idx,
doc='Temperature splitting constraints')
def temperature_splitting(b, t, o):
return self.properties_out[t].temperature == (
self.properties[t, o].temperature)
def _split_pressure(self):
"""
Construct contraints equating inlet pressure to outlet pressure.
Args:
None
Returns:
None
"""
# Equate pressures
@self.Constraint(self.time,
self.outlet_idx,
doc='Pressure splitting constraints')
def pressure_splitting(b, t, o):
return self.properties_out[t].pressure == (
self.properties[t, o].pressure)
[docs] def model_check(blk):
"""
Calls model checks on all associated Property Blocks.
Args:
None
Returns:
None
"""
# Try property block model check
try:
blk.parent_block().property_module.model_check(blk.properties)
except AttributeError:
logger.warning('{} Property package has no model check. To correct'
' this, add a model_check method to the associated '
'PropertyBlock class'.format(blk.name))
[docs] def initialize(blk, state_args=None, outlvl=0, optarg=None,
solver='ipopt', hold_state=False):
''' Initialisation routine for OutletSplitter (default solver ipopt)
Keyword Arguments:
state_args : a dict of arguments to be passed to the property
package(s) to provide an initial state for
initialization (see documentation of the specific
property package) (default = {}).
outlvl : sets output level of initialisation routine
* 0 = no output (default)
* 1 = return solver state for each step in routine
* 2 = include solver output infomation (tee=True)
optarg : solver options dictionary object (default=None)
solver : str indicating whcih solver to use during
initialization (default = 'ipopt')
hold_state : flag indicating whether the initialization routine
should unfix any state variables fixed during
initialization (default=False).
* True = state varaibles are not unfixed, and
a dict of returned containing flags for
which states were fixed during
initialization.
* False = state variables are unfixed after
initialization by calling the
relase_state method
Returns:
If hold_states is True, returns a dict containing flags for
which states were fixed during initialization.
'''
# Set solver options
opt = SolverFactory(solver)
opt.options = optarg
# Check state arguments
state_args = {} if state_args is None else state_args
# Initialize property blocks
flags = blk.properties.initialize(outlvl=outlvl-1,
optarg=optarg,
solver=solver,
hold_state=hold_state,
**state_args)
if outlvl > 0:
logger.info('{} Initialisation Complete'.format(blk.name))
if hold_state:
return flags
[docs] def release_state(blk, flags, outlvl=0):
'''
Method to relase state variables fixed during initialisation.
Keyword Arguments:
flags : dict containing information of which state variables
were fixed during initialization, and should now be
unfixed. This dict is returned by initialize if
hold_state=True.
outlvl : sets output level of of logging (default=0)
'''
blk.properties.release_state(flags, outlvl=outlvl-1)