Source code for idaes.core.ports

##############################################################################
# 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)