Source code for idaes.core.unit_model

##############################################################################
# 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".
##############################################################################
"""
Base clase for unit models
"""
from __future__ import absolute_import  # disable implicit relative imports
from __future__ import division, print_function

import logging

import pyomo.environ as pe
from pyomo.environ import SolverFactory
from pyomo.network import Port
from pyomo.opt import TerminationCondition
from pyomo.common.config import ConfigValue, In

from .process_base import declare_process_block_class, ProcessBlockData
from .ports import InletMixer, OutletSplitter

__author__ = "John Eslick, Qi Chen, Andrew Lee"


__all__ = ['UnitBlockData', 'UnitBlock']

# Set up logger
logger = logging.getLogger('idaes.core')
unit_logger = logging.getLogger('idaes.unit_model')


[docs]@declare_process_block_class("UnitBlock") class UnitBlockData(ProcessBlockData): """ This is the class for process unit operations models. These are models that would generally appear in a process flowsheet or superstructure. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare("dynamic", ConfigValue( default='use_parent_value', domain=In(['use_parent_value', True, False]), description="Dynamic model flag", doc="""Indicates whether this model will be dynamic or not (default = 'use_parent_value'). 'use_parent_value' - get flag from parent (default = False) True - set as a dynamic model False - set as a steady-state model"""))
[docs] def build(self): """ General build method for UnitBlockData. This method calls a number of sub-methods which automate the construction of expected attributes of unit models. Inheriting models should call `super().build`. Args: None Returns: None """ # Set up dynamic flag and time domain self._setup_dynamics()
[docs] def is_process_unit(self): """Tag to indicate that this object is a process unit.""" return True
# FIXME [Qi]: needs to be updated to look for appropriate flow variables
[docs] def display_flows(self): """Display component flow variables associated with the UnitBlockData.""" for var in self.component_objects(ctype=pe.Var): if var.local_name.startswith('flow_') or var.local_name == 'flow' \ or var.local_name.startswith('fc_'): var.display()
# FIXME [Qi]: needs to be updated to look for appropriate flow variables
[docs] def display_total_flows(self): """Display total flow variables associated with the UnitBlockData.""" for var in self.component_objects(ctype=pe.Var): if var.local_name.startswith('total_flow_') or \ var.local_name == 'total_flow': var.display()
# FIXME [Qi]: needs to be updated to look for appropriate variables
[docs] def display_T(self): """Display temperature variables associated with the UnitBlockData.""" for var in self.component_objects(ctype=pe.Var): if var.local_name.startswith('T_') or \ var.local_name in ('T', 'Tin', 'Tout'): var.display()
# FIXME [Qi]: needs to be updated to look for appropriate variables
[docs] def display_P(self): """Display pressure variables associated with the UnitBlockData.""" for var in self.component_objects(ctype=pe.Var): if var.local_name.startswith('P_') or \ var.local_name in ('P', 'Pin', 'Pout'): var.display()
[docs] def display_variables(self, simple=True, descend_into=True): """Display all variables associated with the UnitBlockData. Args: simple (bool, optional): Print a simplified version showing only variable values. """ if not simple: for var in self.component_objects(ctype=pe.Var, descend_into=descend_into): var.display() else: for vardata in self.component_data_objects(ctype=pe.Var, descend_into=False): print("{}: {}".format(vardata.local_name, vardata.value))
def _setup_dynamics(self): """ This method automates the setting of the dynamic flag and time domain for unit models. Performs the following: 1) Determines if this is a top level flowsheet 2) Gets dynamic flag from parent if not top level, or checks validity of argument provided 3) Gets time domain from parent, or creates domain if top level model 4) Checks include_holdup flag if present and dynamic = True Args: None Returns: None """ # Check the dynamic flag, and retrieve if necessary if self.config.dynamic == 'use_parent_value': # Get dynamic flag from parent try: self.config.dynamic = self.parent_block().config.dynamic except AttributeError: # If parent does not have dynamic flag, raise Exception raise AttributeError('{} has a parent model ' 'with no dynamic attribute.' .format(self.name)) # Check for case when dynamic=True, but parent dynamic=False if (self.config.dynamic and not self.parent_block().config.dynamic): raise ValueError('{} trying to declare a dynamic model within ' 'a steady-state flowsheet. This is not ' 'supported by the IDAES framework. Try ' 'creating a dynamic flowsheet instead, and ' 'declaring some models as steady-state.' .format(self.name)) # Try to get reference to time object from parent try: object.__setattr__(self, "time", self.parent_block().time) except AttributeError: raise AttributeError('{} has a parent model ' 'with no time domain'.format(self.name)) # Check include_holdup, if present if self.config.dynamic: if hasattr(self.config, "include_holdup"): if not self.config.include_holdup: # Dynamic model must have include_holdup = True logger.warning('{} Dynamic models must have ' 'include_holdup = True. ' 'Overwritting argument.' .format(self.name)) self.config.include_holdup = True
[docs] def build_inlets(self, holdup=None, inlets=None, num_inlets=None): """ This is a method to build inlet Port objects in a unit model and connect these to holdup blocks as needed. This method supports an arbitary number of inlets and holdup blocks, and works for both simple (0D) and 1D IDAES holdup blocks. Keyword Args: holdup = holdup block to which inlets are associated. If left None, assumes a default holdup (default = None). inlets = argument defining inlet names (default: None). inlets may be None or list. - None - assumes a single inlet. - list - use names provided in list for inlets (can be other iterables, but not a string or dict) num_inlets = argument indication number (int) of inlets to construct (default = None). Not used if inlets arg is provided. - None - use inlets arg instead - int - Inlets will be named with sequential numbers from 1 to num_inlets. Returns: A Pyomo Port object and assoicated components. """ # Check holdup argument, and get holdup block if holdup is None: # If None, assume default names try: hblock = self.holdup inlet_name = 'inlet' except AttributeError: raise AttributeError('{} Invalid holdup argument. Unit model ' 'contains no attribute holdup.' .format(self.name)) else: # Otherwise, use named holdup and inlet try: hblock = getattr(self, holdup) inlet_name = holdup+'_inlet' except AttributeError: raise AttributeError('{} Invalid holdup argument. Unit model ' 'contains no attribute {}.' .format(self.name, holdup)) # Validate inlets argument and create inlet_list if needed inlet_list = [] if inlets is None: if num_inlets is not None: # Create list of integers as strings and put in inlet_list inlet_list = list(map(str, range(1, num_inlets+1))) else: # No arguments provided, assume single default inlet inlet_list = [None] else: # List of named inlets, should be iterable but not string or dict try: # Test iterable iter(inlets) # Raise exception if string or dict if isinstance(inlets, (str, dict)): raise TypeError('{} Invalid inlets argument. Must be ' 'iterable but not a string or a dict.' .format(self.name)) # Ensure that all elements are strings for i in inlets: inlet_list += [str(i)] except TypeError: # Unrecognised type for inlets raise TypeError('{} Unrecognised type for inlets argument.' .format(self.name)) # Check if both inlets and num_inlets args were provided if num_inlets is not None: # Log a warning if num_inlets != len(inlets): # If args don't agree, raise Exception raise ValueError("{} provided with both inlets and " "num_inlets arguments which do " "not agree. Check the values assigned to " "these. It is also only necessary to " "specify one of these arguments." .format(self.name)) else: # Otherwise just log an debug message logger.debug("{} provided with both inlets and " "num_inlets arguments - num_inlets will be " "ignored.".format(self.name)) # Check for duplicate inlet names if inlets is not None: if any(inlet_list.count(x) > 1 for x in inlet_list): raise ValueError('{} Duplicate inlet name found.' .format(self.name)) if len(inlet_list) > 1: # Multiple inlets to holdup, so call InletMixer hblock.inlet_mixer = InletMixer(doc="Mixer for multiple inlets.", inlets=inlet_list) # Build inlet port object and populate if len(inlet_list) == 1: # Only one inlet, index only by time def inlet_rule(b, t): try: return hblock.properties_in[t].declare_port_members() except AttributeError: if hasattr(hblock, "ldomain"): if hblock.config.flow_direction is "forward": prop_key = (t, hblock.ldomain.first()) else: prop_key = (t, hblock.ldomain.last()) else: prop_key = t return hblock.properties[prop_key].declare_port_members() i = Port(self.time, rule=inlet_rule, doc="Inlet port object") setattr(self, inlet_name, i) else: # Multiple inlets, need to index conenctor # Create inlet port and populate later i = Port(self.time, hblock.inlet_mixer.inlet_idx, noruleinit=True, doc="Inlet port object") setattr(self, inlet_name, i) inlet_obj = getattr(self, inlet_name) # Get the assoicated property block from inlet_mixer pblock = hblock.inlet_mixer.properties # Iterate over time and inlets for i in hblock.inlet_mixer.inlet_idx: for t in self.time: # Get the member of the port to add members = pblock[t, i].declare_port_members() for obj in members: inlet_obj[t, i].add(members[obj], obj)
[docs] def build_outlets(self, holdup=None, outlets=None, num_outlets=None, split_type='flow'): """ This is a method to build outlet Port objects in a unit model and connect these to holdup blocks as needed. This method supports an arbitary number of outlets and holdup blocks, and works for both simple (0D) and 1D IDAES holdup blocks. Keyword Args: holdup = holdup block to which inlets are associated. If left None, assumes a default holdup (default = None). outlets = argument defining outlet names (default: None). outlets may be None or list. - None - assumes a single outlet. - list - use names provided in list for outlets (can be other iterables, but not a string or dict) num_outlets = argument indication number (int) of outlets to construct (default = None). Not used if outlets arg is provided. - None - use outlets arg instead - int - Outlets will be named with sequential numbers from 1 to num_outlets. split_type = argument defining method to use to split outlet flow in case of multiple outlets (default = 'flow'). - 'flow' - outlets are split by total flow - 'phase' - outlets are split by phase - 'component' - outlets are split by component - 'total' - outlets are split by both phase and component - 'duplicate' - all outlets are duplicates of the total outlet stream. Returns: A Pyomo Port object and assoicated components. """ # Check holdup argument, and get holdup block if holdup is None: # If None, assume default names try: hblock = self.holdup outlet_name = 'outlet' except AttributeError: raise AttributeError('{} Invalid holdup argument. Unit model ' 'contains no attribute holdup.' .format(self.name)) else: # Otherwise, use named holdup and outlet try: hblock = getattr(self, holdup) outlet_name = holdup+'_outlet' except AttributeError: raise AttributeError('{} Invalid holdup argument. Unit model ' 'contains no attribute {}.' .format(self.name, holdup)) # Validate inlets argument and create outlet_list outlet_list = [] if outlets is None: if num_outlets is not None: # Create list of integers as strings and put in outlet_list outlet_list = list(map(str, range(1, num_outlets+1))) else: # No arguments provided, assume single default outlet outlet_list = [None] else: # List of named outlets, should be iterable but not string or dict try: # Test iterable iter(outlets) # Raise exception if string or dict if isinstance(outlets, (str, dict)): raise TypeError('{} Invalid outlets argument. Must be ' 'iterable but not a string or a dict.' .format(self.name)) # Ensure that all elements are strings for o in outlets: outlet_list += [str(o)] except TypeError: # Unrecognised type for outlets raise TypeError('{} Unrecognised type for outlets argument.' .format(self.name)) # Check if both outlets and num_outlets args were provided if num_outlets is not None: # Log a warning if num_outlets != len(outlets): # If args don't agree, raise Excpetion raise ValueError("{} provided with both outlets and " "num_outlets arguments which do " "not agree. Check the values assigned to " "these. It is also only necessary to " "specify one of these arguments." .format(self.name)) else: # Otherwise just log an info message logger.debug("{} provided with both outlets and " "num_outlets arguments - num_outlets will be " "ignored.".format(self.name)) # Check for duplicate outlet names if outlets is not None: if any(outlet_list.count(x) > 1 for x in outlet_list): raise ValueError('{} Duplicate outlet name found.' .format(self.name)) # Check that split_type argument is valid valid_args = ['flow', 'phase', 'component', 'total', 'duplicate'] if split_type not in valid_args: raise ValueError('{} Unrecognised value for split_type ' 'argument.'.format(self.name)) if len(outlet_list) > 1: # Multiple outlets to holdup, so check if splitter is required # If split_type is duplicate, no splitter is required if split_type != 'duplicate': # Call OutletSplitter hblock.outlet_splitter = OutletSplitter( doc="Mixer for multiple inlets.", outlets=outlet_list, split_type=split_type) # Build outlet port object and populate if len(outlet_list) == 1: # Only one outlet, index only by time def outlet_rule(b, t): try: return hblock.properties_out[t].declare_port_members() except AttributeError: if hasattr(hblock, "ldomain"): if hblock.config.flow_direction is "forward": prop_key = (t, hblock.ldomain.last()) else: prop_key = (t, hblock.ldomain.first()) else: prop_key = t return hblock.properties[prop_key].declare_port_members() o = Port(self.time, rule=outlet_rule, doc="Outlet port object") setattr(self, outlet_name, o) else: # Multiple inlets, need to index conenctor # Create outlet port and populate later o = Port(self.time, hblock.outlet_splitter.outlet_idx, noruleinit=True, doc="Outlet port object") setattr(self, outlet_name, o) outlet_obj = getattr(self, outlet_name) # Check split_type to populate outlet if split_type != 'duplicate': # Get the assoicated property block from outlet_mixer pblock = hblock.outlet_splitter.properties else: # Outlets are duplicates, so no splitter pblock = hblock.properties_out # Iterate over time and inlets for i in hblock.outlet_splitter.outlet_idx: for t in self.time: # Get the member of the port to add members = pblock[t, i].declare_port_members() for obj in members: outlet_obj[t, i].add(members[obj], obj)
[docs] def model_check(blk): """ This is a general purpose initialization routine for simple unit models. This method assumes a single Holdup block called holdup and tries to call the model_check method of the holdup block. If an AttributeError is raised, the check is passed. More complex models should overload this method with a model_check suited to the particular application, especially if there are multiple Holdup blocks present. Args: None Returns: None """ # Run holdup block model checks try: blk.holdup.model_check() except AttributeError: pass
[docs] def initialize(blk, state_args=None, outlvl=0, solver='ipopt', optarg={'tol': 1e-6}): ''' This is a general purpose initialization routine for simple unit models. This method assumes a single Holdup block called holdup, and first initializes this and then attempts to solve the entire unit. More complex models should overload this method with their own initialization routines, 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 = return solver state for each step in subroutines * 3 = include solver output infomation (tee=True) optarg : solver options dictionary object (default={'tol': 1e-6}) solver : str indicating whcih solver to use during initialization (default = 'ipopt') Returns: None ''' # Set solver options if outlvl > 3: stee = True else: stee = False opt = SolverFactory(solver) opt.options = optarg # --------------------------------------------------------------------- # Initialize holdup block flags = blk.holdup.initialize(outlvl=outlvl-1, optarg=optarg, solver=solver, state_args=state_args) if outlvl > 0: unit_logger.info('{} Initialisation Step 1 Complete.' .format(blk.name)) # --------------------------------------------------------------------- # Solve unit results = opt.solve(blk, tee=stee) if outlvl > 0: if results.solver.termination_condition == \ TerminationCondition.optimal: unit_logger.info('{} Initialisation Step 2 Complete.' .format(blk.name)) else: unit_logger.warning('{} Initialisation Step 2 Failed.' .format(blk.name)) # --------------------------------------------------------------------- # Release Inlet state blk.holdup.release_state(flags, outlvl-1) if outlvl > 0: unit_logger.info('{} Initialisation Complete.'.format(blk.name))