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