Source code for idaes.core.stream

##############################################################################
# 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 IDAES streams
"""
from __future__ import absolute_import  # disable implicit relative imports
from __future__ import division, print_function

from six import iteritems
import itertools
import logging
import sys

from pyomo.environ import Constraint, value
from pyomo.common.config import ConfigValue
from pyomo.core.base.misc import tabular_writer
from .process_base import declare_process_block_class, ProcessBlockData
from .util.config import is_port


__author__ = "Andrew Lee"


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


[docs]class VarDict(dict): """ This class creates an object which behaves like a Pyomo IndexedVar. It is used to contain the separate Vars contained within IndexedPorts, and make them look like a single IndexedVar. This class supports the fix, unfix and display attributes. """
[docs] def display(self, side='source', ostream=None, prefix=""): """ Print component information Args: side : which side of port to display (default = 'source'). Valid values are 'source' or 'destination'. ostream : output stream (default = None) prefix : str to append to each line of output (default = '') Returns: None """ # Validate side argument, and set key for member tuple if side == 'source': ds = 0 elif side == 'destination': ds = 1 else: raise ValueError('Unrecoginsed argument side = {}. Must be ' 'either "source" or "destination".' .format(side)) if ostream is None: ostream = sys.stdout tab = " " ostream.write(prefix+self[0][ds].local_name+" : ") if self[0][ds].doc is not None: ostream.write(self[0][ds].doc+'\n'+prefix+tab) tv_pairs = {} for t in self: tv_pairs[t] = self[t][ds] _attr = [("Size", len(self[t][ds])), ("Index", self[t][ds]._index if self[t][ds].is_indexed() else None), ] ostream.write(", ".join("%s=%s" % (k, v) for k, v in _attr)) ostream.write("\n") tabular_writer(ostream, prefix+tab, ((k, v) for k, v in iteritems(tv_pairs)), ("Lower", "Value", "Upper", "Fixed", "Stale", "Domain"), lambda k, v: [value(v.lb), v.value, value(v.ub), v.fixed, v.stale, v.domain])
[docs] def fix(self, value=None, side='destination'): """ Method to fix Vars. Args: value : value to use when fixing Var (default = None). side : side of port to fix (default = 'destination'). Valid values are 'source', 'destination' or 'all'. Returns: None """ # Check side for valid value if side not in ['all', 'source', 'destination']: raise ValueError('Unrecoginsed argument side = {}. Must be ' 'either "source", "destination" or "all".' .format(side)) # Call Pyomo fix attribute for all members of VarDict for k in self.keys(): if side in ['all', 'source']: if value is None: self[k][0].fix() else: self[k][0].fix(value) if side in ['all', 'destination']: if value is None: self[k][1].fix() else: self[k][1].fix(value)
[docs] def unfix(self, side='destination'): """ Method to unfix Vars. Args: value : value to use when fixing Var (default = None) side : side of port to fix (default = 'destination'). Valid values are 'source', 'destination' or 'all'. Returns: None """ # Check side for valid value if side not in ['all', 'source', 'destination']: raise ValueError('Unrecoginsed argument side = {}. Must be ' '"all", "source" or "destination".' .format(side)) # Call Pyomo unfix attribute for all members of VarDict for k in self.keys(): if side in ['all', 'source']: self[k][0].unfix() if side in ['all', 'destination']: self[k][1].unfix()
[docs]@declare_process_block_class("Stream") class StreamData(ProcessBlockData): """ This is the class for process streams. These are blocks that connect two unit models together. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare("source", ConfigValue( domain=is_port, description="Source Port", doc="""Pyomo Port object representing the source of the process stream.""")) CONFIG.declare("source_idx", ConfigValue( default=None, domain=str, description="Source Port index", doc="""Key of indexed Port to use for source (if applicable). default = None.""")) CONFIG.declare("destination", ConfigValue( domain=is_port, description="Destination Port", doc="""Pyomo Port object representing the destination of the process stream.""")) CONFIG.declare("destination_idx", ConfigValue( default=None, domain=str, description="Destination Port index", doc="""Key of indexed Port to use for destination (if applicable). default = None."""))
[docs] def build(self): """ General build method for StreamDatas. Inheriting models should call super().build. Args: None Returns: None """ # Setup time domain self._set_time_domain() # Check that port are compatible self._check_ports() # Build constraints self._build_constraints() # Add variable references self._build_vars()
[docs] def activate(self, var=None): """ Method for activating Constraints in Stream. If not provided with any arguments, this activates the entire Stream block. Alternatively, it may be provided with the name of a variable in the Stream, in which case only the Constraint associated with that variable will be activated. Args: var : name of a variable in the Stream for which the corresponding Constraint should be activated (default = None). Returns: None """ if var is None: # If no var argument, activate Block super(StreamData, self).activate() else: c_name = var+"_equality" try: # Try to call activate method on associated Constraint c = getattr(self, c_name) c.activate() except AttributeError: # Raise Exception if var does not exist in Stream raise AttributeError('{} has no variable named {}' .format(self.name, var))
[docs] def deactivate(self, var=None): """ Method for deactivating Constraints in Stream. If not provided with any arguments, this deactivates the entire Stream block. Alternatively, it may be provided with the name of a variable in the Stream, in which case only the Constraint associated with that variable will be deactivated. Args: var : name of a variable in the Stream for which the corresponding Constraint should be deactivated (default = None). Returns: None """ if var is None: # If no var argument, deactivate Block super(StreamData, self).deactivate() else: c_name = var+"_equality" try: # Try to call deactivate method on associated Constraint c = getattr(self, c_name) c.deactivate() except AttributeError: # Raise Exception if var does not exist in Stream raise AttributeError('{} has no variable named {}' .format(self.name, var))
[docs] def converged(self, tolerance=1e-6): """ Check if the values on both sides of a Stream are converged. Args: tolerance : tolerance to use when checking if Stream is converged. (default = 1e-6). Returns: A Bool indicating whether the Stream is converged """ converged = True for t in self.time: if self.config.source_idx is None: s_obj = self.config.source[t] else: s_obj = self.config.source[t, self.config.source_idx] if self.config.destination_idx is None: d_obj = self.config.destination[t] else: d_obj = self.config.destination[t, self.config.destination_idx] for k in s_obj.vars: try: if not s_obj.vars[k].is_indexed(): if (abs(value(s_obj.vars[k] - d_obj.vars[k])) > tolerance): converged = False else: for m in s_obj.vars[k]: if (abs(value(s_obj.vars[k][m]-d_obj.vars[k][m])) > tolerance): converged = False except: # If an exception occurs, Stream is not converged. # This is mainly for uninitialized Vars in the Ports converged = False if not converged: break if not converged: break return converged
[docs] def display(self, side='source', display_constraints=False, tolerance=1e-6, ostream=None, prefix=""): """ Display the contents of Stream Block. Args: side : side of Stream to display values from (default = 'soruce'). Valid values are 'source' and 'destination'. display_constraints : indicates whether to display Constraint information (default = False). tolerance : tolerance to use when checking if Stream is converged. (default = 1e-6). ostream : output stream (default = None) prefix : str to append to each line of output (default = '') """ # Validate side argument if side not in ['source', 'destination']: raise ValueError('{} unrecoginsed argument side = {}. Must be ' 'either "source" or "destination".' .format(self.name, side)) if ostream is None: ostream = sys.stdout tab = " " ostream.write(prefix+self.local_name+" : ") if self.doc is not None: ostream.write(self.doc+'\n'+prefix+tab) # Check if Stream is converged converged = self.converged(tolerance=tolerance) ostream.write("Converged : "+str(converged)+"\n") # Collect data from Port object kv_pairs = {} for t in self.time: if side == 'source': if self.config.source_idx is None: c_obj = self.config.source[t] else: c_obj = self.config.source[t, self.config.source_idx] else: if self.config.destination_idx is None: c_obj = self.config.destination[t] else: c_obj = self.config.destination[ t, self.config.destination_idx] kv_pairs[t] = c_obj # Generate print expression and call Pyomo tabular_writer def _line_generator(k, v): for _k, _v in sorted(iteritems(v.vars)): if _v is None: _val = '-' elif not hasattr(_v, 'is_indexed') or not _v.is_indexed(): _val = str(value(_v)) else: _val = "{%s}" % (', '.join('%r: %r' % ( x, value(_v[x])) for x in sorted(_v._data)),) yield _k, _val ostream.write(prefix+tab+"Variables"+"\n") tabular_writer(ostream, prefix+tab, ((k, v) for k, v in iteritems(kv_pairs)), ("Name", "Value"), _line_generator) # If display_constraints = True, display Constraints if display_constraints: ostream.write("\n"+prefix+tab+"Constraints"+"\n") for c in self.component_objects(Constraint, descend_into=False): c.display(ostream=ostream, prefix=prefix+tab)
def _set_time_domain(self): """ Method to collect time domain from parent object. Args: None Returns: None """ # 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)) def _check_ports(self): """ Method to check consistency of Ports. Args: None Returns: None """ # Check that source and destination are specified if self.config.source is None: raise ValueError("{} source argument not provided. A Port " "object must be provided as the source point " "for the Stream.".format(self.name)) if self.config.destination is None: raise ValueError("{} destination argument not provided. A " "Port object must be provided as the " "destination point for the Stream." .format(self.name)) # Get Port objects tt = self.time.first() if self.config.source_idx is not None: try: src_port = \ self.config.source[tt, self.config.source_idx] except KeyError: raise KeyError("{} source Port is not indexed by outlet " "name, however a source_idx argument was " "provided. Check the indexing of the source " "Port and correct this or remove the " "source_idx argument".format(self.name)) else: try: src_port = self.config.source[tt] except KeyError: raise KeyError("{} source Port object is indexed by " "outlet name but no source_idx argument was " "provided. An index to use for the source " "Port must be provided".format(self.name)) if self.config.destination_idx is not None: try: dst_port = \ self.config.destination[tt, self.config.destination_idx] except KeyError: raise KeyError("{} destination Port is not indexed by " "inlet name, however a destination_idx argument" " was provided. Check the indexing of the " "destination Port and correct this or " "remove the destination_idx argument" .format(self.name)) else: try: dst_port = self.config.destination[tt] except KeyError: raise KeyError("{} destination Port object is indexed by " "inlet name but no destination_idx argument was" " provided. An index to use for the destination" " Port must be provided".format(self.name)) # Check that source and destination have the same number of members if len(src_port.vars) != len(dst_port.vars): raise AttributeError('{} source and destination ports ' 'are not compatible. Ports have ' 'different numbers of memebers.' .format(self.name)) # Check consistency of members between source and destination for k in src_port.vars.keys(): # Check that all members of source appear in destination if k not in dst_port.vars.keys(): raise AttributeError('{} source and destination ports ' 'are not compatible. Key {} from source ' 'does not exist in destination.' .format(self.name, k)) # Check that both source and destination have same indexing sets s_idx = self._get_var_idx(self.config.source, self.config.source_idx, k) d_idx = self._get_var_idx(self.config.destination, self.config.destination_idx, k) if s_idx != d_idx: raise AttributeError('{} source and destination ports ' 'are not compatible. Key {} has ' 'different indexing sets between source ' 'and destination.' .format(self.name, k)) def _build_constraints(self): """ Method to construct Constraints in Stream. Args: None Returns: None """ # Create indexing tuple for Port objects if self.config.source_idx is None: tk = self.time.first() else: tk = (self.time.first(), self.config.source_idx) # Iterate over all members of source port for k in self.config.source[tk].vars.keys(): c_name = k+"_equality" # Generate expression and Constraint object if not self.config.source[tk].vars[k].is_indexed(): # Member has no additional indexing sets def c_rule(b, t): if self.config.source_idx is None: skey = t else: skey = (t, self.config.source_idx) if self.config.destination_idx is None: dkey = t else: dkey = (t, self.config.destination_idx) return (self.config.source[skey].vars[k] == self.config.destination[dkey].vars[k]) c = Constraint(self.time, rule=c_rule) else: # Member is indexed var_idx = self._get_var_idx(self.config.source, self.config.source_idx, k) def c_rule(b, t, *args): if self.config.source_idx is None: skey = t else: skey = (t, self.config.source_idx) if self.config.destination_idx is None: dkey = t else: dkey = (t, self.config.destination_idx) return (self.config.source[skey].vars[k][args] == self.config.destination[dkey].vars[k][args]) c = Constraint(self.time, var_idx, rule=c_rule) # Add Constraint to Stream setattr(self, c_name, c) def _build_vars(self): """ Method to construct VarDict objects for all members of Stream. Args: None Returns: None """ # Create sample indexing tuple for Port objects if self.config.source_idx is None: tk = self.time.first() else: tk = (self.time.first(), self.config.source_idx) # Iterate over all members of source port for k in self.config.source[tk].vars.keys(): # Check for naming conflict, and Except if one found if hasattr(self, k): raise AttributeError('{} already has an attribute with name ' '{}. This is likely due to a poor choice' ' of names in the property package.' .format(self.name, k)) else: # Create new VarDict object setattr(self, k, VarDict()) v_dict = getattr(self, k) var_idx = self._get_var_idx(self.config.source, self.config.source_idx, k) # Iterate over time for t in self.time: # Get indexing tuple for Port object if self.config.source_idx is None: src_keys = t else: src_keys = (t, self.config.source_idx) if self.config.destination_idx is None: dst_keys = t else: dst_keys = (t, self.config.destination_idx) # Get Vars from Ports and add to VarDict if not self.config.source[tk].vars[k].is_indexed(): # Member has no additional indexing sets v_dict[t] = [self.config.source[src_keys].vars[k], self.config.destination[dst_keys].vars[k]] elif isinstance(var_idx, list): # Member has multiple additional indexing sets key_pairs = list(itertools.product( *var_idx)) for i in key_pairs: v_idx = tuple([t]+list(i)) v_dict[v_idx] = [ self.config.source[src_keys].vars[k][i], self.config.destination[dst_keys].vars[k][i]] else: # Memeber has a single additional indexing set for i in var_idx: v_dict[t, i] = [ self.config.source[src_keys].vars[k][i], self.config.destination[dst_keys].vars[k][i]] def _get_var_idx(self, port_obj, port_idx, port_memb): """ Method to get list of indexing sets from a sample member of a Port object. Args: port_obj : Conenctor object port_idx : Index of Port object to use port_memb : Member of Port to be checked Returns: list of indexing sets of port_memb """ # Get indexing tuple for a sample PortData object if port_idx is None: tk = self.time.first() else: tk = (self.time.first(), port_idx) # Check for indexing sets of member if not port_obj[tk].vars[port_memb].is_indexed(): # Member is not indexed var_idx = None elif port_obj[tk].vars[port_memb]._implicit_subsets is None: # Member has a single index var_idx = port_obj[tk].vars[port_memb]._index else: # Member has multiple indices var_idx = port_obj[tk].vars[port_memb]._implicit_subsets return var_idx