Source code for idaes.core.property_base

##############################################################################
# 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".
##############################################################################
"""
This module contains classes for property blocks and property parameter blocks.
"""
from __future__ import division

# Import Python libraries
import inspect
import logging

# Import Pyomo libraries
from pyomo.common.config import ConfigValue, In

# Other third-party
import six

# Import IDAES cores
from idaes.core.process_block import ProcessBlock
from idaes.core import ProcessBlockData

# Some more information about this module
__author__ = "Andrew Lee"

__all__ = ['PropertyBlockDataBase', 'PropertyParameterBase',
           'PropertyMetadata', 'PropertyClassMetadata',
           'HasPropertyClassMetadata']

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


[docs]class PropertyMetadata(dict): """Simple container for property parameter metadata. """ def __init__(self, name, method=None, units=None): super(PropertyMetadata, self).__init__({ 'name': name, 'method': method or '', 'units': units or '' })
[docs]class PropertyClassMetadata(object): """Simple container for metadata about the property class, which contains properties that can each be associated with :class:`PropertyMetadata`. """ def __init__(self): self.default_units = {} self.properties = {}
[docs] def add_property_metadata(self, pmeta): """Add property metadata. Args: pmeta (PropertyMetadata): Add this """ self.properties[pmeta['name']] = pmeta
[docs]class HasPropertyClassMetadata(object): """Interface for something with PropertyClassMetadata. """
[docs] @classmethod def get_metadata(cls): """Get the metadata. Returns: PropertyClassMetadata: The metadata """ pass
[docs]class PropertyParameterBase(ProcessBlockData, HasPropertyClassMetadata): """ This is the base class for property parameter blocks. These are blocks that contain a set of parameters associated with a specific property package, and are linked to by all instances of that property package. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare("default_arguments", ConfigValue( description="Default arguments to use with Property Package"))
[docs] def build(self): """ General build method for PropertyParameterBlocks. Inheriting models should call super().build. Args: None Returns: None """ # Get module reference and store on block frm = inspect.stack()[1] self.property_module = inspect.getmodule(frm[0])
[docs] @classmethod def get_metadata(cls): """Get property parameter metadata. Instantiates a "dummy" instance of the containing class. Returns: PropertyClassMetadata: The metadata Raises: Any exception raised by the constructor, if for some reason this class cannot be instantiated. """ class BlockMock(object): _block_data_config = {} try: obj = cls(BlockMock()) except Exception as err: logger.error('Unable to create instance of class "{}", for ' 'the purpose of retrieving its metadata: {}' .format(cls.__name__, err)) raise pcmeta = PropertyClassMetadata() try: sp = obj.get_supported_properties() for k, v in six.iteritems(sp): pcmeta.add_property_metadata(PropertyMetadata(k, **v)) except (NotImplementedError, AttributeError): # AttributeError may occur if no attr "self.name" pass try: pcmeta.default_units = obj.get_package_units() except (NotImplementedError, AttributeError): # AttributeError may occur if no attr "self.name" pass return pcmeta
[docs] def get_supported_properties(self): """ Method to return a dictionary of properties supported by this package and their assoicated construction methods and units of measurement. This method should return a dict with keys for each supported property. For each property, the value should be another dict which may contain the following keys: - 'method': (required) the name of a method to construct the property as a str, or None if the property will be constructed by default. - 'units': (optional) units of measurement for the property. This default method is a placeholder and should be overloaded by the package developer. This method will return an Exception if not overloaded. Args: None Returns: A dict with supported properties as keys. """ raise NotImplementedError('{} property package has not implemented the' ' get_supported_properties method. Contact ' 'the property package developer' .format(self.name))
[docs] def get_package_units(self): """ Method to return a dictionary of default units of measurement used in the property package. This is used to populate doc strings for variables which derive from the property package (such as flows and volumes). This method should return a dict with keys for the quantities used in the property package (as strs) and values of their default units as strs. The quantities used by the framewokr are (all optional): - 'time' - 'length' - 'mass' - 'amount' - 'temperature' - 'energy' - 'current' - 'luminous intensity' This default method is a placeholder and should be overloaded by the package developer. This method will return an Exception if not overloaded. Args: None Returns: A dict with supported properties as keys and tuples of (method, units) as values. """ raise NotImplementedError('{} property package has not implemented the' ' get_package_units method. Contact ' 'the property package developer' .format(self.name))
[docs]class PropertyBlockBase(ProcessBlock): """ This is the base class for property block objects. These are used when constructing the SimpleBlock or IndexedBlock which will contain the PropertyData objects, and contains methods that can be applied to multiple PropertyBlockData objects simultaneously. """
[docs] def initialize(self, *args): """ This is a default initialization routine for PropertyBlocks to ensure that a routine is present. All PropertyBlockData classes should overload this method with one suited to the particular property package This method prints a warning and then ends. Args: None Returns: None """ logger.warning('{} Property package does not define an initialization' 'method. PropertyBlock has not been initialized' .format(self.name))
[docs]class PropertyBlockDataBase(ProcessBlockData): """ This is the base class for property block data objects. These are blocks that contain the Pyomo components associated with calculating a set of thermophysical, transport and reacion properties for a given material. """ # Create Class ConfigBlock CONFIG = ProcessBlockData.CONFIG() CONFIG.declare("parameters", ConfigValue( description="""A reference to an instance of the Property Parameter Block associated with this property package.""")) CONFIG.declare("has_sum_fractions", ConfigValue( default=False, domain=In([True, False]), description="Sum of material fractions flag", doc="""Flag indicating whether this property block should include constraints for the sum of material fractions (e.g. sum of mole fractions == 1) (default=False). """)) CONFIG.declare("calculate_equilibrium_reactions", ConfigValue( default=True, domain=In([True, False]), description="Equilibrium reaction constraint flag", doc="""Flag indicating whether equilibrium reaction constraints should be constructed in this property block (default=True). """)) CONFIG.declare("calculate_phase_equilibrium", ConfigValue( default=True, domain=In([True, False]), description="Phase equilibrium constraint flag", doc="""Flag indicating whether phase equilibrium constraints should be constructed in this property block (default=True). """))
[docs] def build(self): """ General build method for PropertyBlockDatas. Inheriting models should call super().build. Args: None Returns: None """ pass
def __getattr__(self, attr): """ This method is used to avoid generating unnecessary property calculations in property blocks. __getattr__ is called whenever a property is called for, and if a propery does not exist, it looks for a method to create the required property, and any associated components. Create a property calculation if needed. Return an attrbute error if attr == 'domain' or starts with a _ . The error for _ prevents a recursion error if trying to get a function to create a property and that function doesn't exist. Pyomo also ocasionally looks for things that start with _ and may not exist. Pyomo also looks for the domain attribute, and it may not exist. This works by creating a property calculation by calling the "_"+attr function. A list of __getattr__ calls is maintained in self.__getattrcalls to check for recursive loops which maybe useful for debugging. This list is cleared after __getattr__ completes successfully. Args: attr: an attribute to create and return. Should be a property component. """ def clear_call_list(self, attr): """Local method for cleaning up call list when a call is handled. Args: attr: attribute currently being handled """ if self.__getattrcalls[-1] == attr: if len(self.__getattrcalls) <= 1: del self.__getattrcalls else: del self.__getattrcalls[-1] else: raise ValueError("{} Trying to remove call {} from __getattr__" " call list, however this is not the most " "recent call in the list ({}). This indicates" " a bug in the __getattr__ calls. Please " "contact the IDAES developers with this bug." .format(self.name, attr, self.__getattrcalls[-1])) # Check that attr is not something we shouldn't touch if attr == "domain" or attr.startswith("_"): # Don't interfere with anything by getting attributes that are # none of my business raise AttributeError('{} {} does not exist, but is a protected ' 'attribute. Check the naming of your ' 'components to avoid any reserved names' .format(self.name, attr)) # Check for recursive calls try: # Check to see if attr already appears in call list if attr in self.__getattrcalls: # If it does, indicates a recursive loop. if attr == self.__getattrcalls[-1]: # attr method is calling itself self.__getattrcalls.append(attr) raise Exception('{} _{} made a recursive call to ' 'itself, indicating a potential ' 'recursive loop. This is generally ' 'caused by the {} method failing to ' 'create the {} component.' .format(self.name, attr, attr, attr)) else: self.__getattrcalls.append(attr) raise Exception('{} a potential recursive loop has been ' 'detected whilst trying to construct {}. ' 'A method was called, but resulted in a ' 'subsequent call to itself, indicating a ' 'recursive loop. This may be caused by a ' 'method trying to access a component out ' 'of order for some reason (e.g. it is ' 'declared later in the same method). See ' 'the __getattrcalls object for a list of ' 'components called in the __getattr__ ' 'sequence.' .format(self.name, attr)) # If not, add call to list self.__getattrcalls.append(attr) except AttributeError: # Creat a list of calls if one does not already exist self.__getattrcalls = [attr] # Get property information from get_supported_properties try: m = self.config.parameters.get_supported_properties() if m is None: raise ValueError('{} Property package get_supported_properties' ' method returned None when trying to create ' '{}. Please contact the developer of the ' 'property package'.format(self.name, attr)) except KeyError: # If attr not in get_supported_properties, assume package does not # support property clear_call_list(self, attr) raise AttributeError('{} {} is not supported by property package ' '(property is not listed in ' 'get_supported_properties).' .format(self.name, attr, attr)) # Get method name from get_supported_properties try: if m[attr]['method'] is None: # If method is none, property should be constructed # by property package, so raise AttributeError clear_call_list(self, attr) raise AttributeError( '{} {} should be constructed automatically ' 'by property package, but is not present. ' 'This can be caused by methods being called ' 'out of order.'.format(self.name, attr)) elif m[attr]['method'] is False: # If method is False, package does not support property # Raise NotImplementedError clear_call_list(self, attr) raise NotImplementedError( '{} {} is not supported by property package ' '(property method is listed as False in ' 'get_supported_properties).' .format(self.name, attr)) elif isinstance(m[attr]['method'], str): # Try to get method name in from PropertyBlock object try: f = getattr(self, m[attr]['method']) except AttributeError: # If fails, method does not exist clear_call_list(self, attr) raise AttributeError( '{} {} get_supported_properties method ' 'returned a name that does not correspond' ' to any method in the property package. ' 'Please contact the developer of the ' 'property package.'.format(self.name, attr)) else: # Otherwise method name is invalid clear_call_list(self, attr) raise ValueError('{} {} get_supported_properties method ' 'returned invalid value for method name. ' 'Please contact the developer of the ' 'property package.' .format(self.name, attr)) except KeyError: # No method key - raise Exception # Need to use an AttributeError so Pyomo.DAE will handle this clear_call_list(self, attr) raise AttributeError('{} get_supported_properties method ' 'does not contain a method for {}. ' 'Please contact the developer of the ' 'property package.'.format(self.name, attr)) # Call attribute if it is callable # If this fails, it should return a meaningful error. if callable(f): try: f() except: # Clear call list and reraise error clear_call_list(self, attr) raise else: # If f is not callable, inform the user and clear call list clear_call_list(self, attr) raise AttributeError('{} has an attribute _{}, however it is not ' 'callable.'.format(self.name, attr)) # Clear call list, and return comp = getattr(self, attr) clear_call_list(self, attr) return comp