##############################################################################
# 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".
##############################################################################
"""
The process_block module simplifies inheritance of Pyomo blocks. The main reason
to subclass a Pyomo block is to create a block that comes with pre-defined model
equations. This is used in the IDAES modeling framework to to create modular
process model blocks.
"""
from __future__ import absolute_import, division, print_function
import sys
import logging
from pyomo.environ import Block
__author__ = "John Eslick"
__all__ = ['ProcessBlock', 'declare_process_block_class']
# Reserved keywords that go to the Block __init__
_block_kwds = ('rule', 'options', 'concrete',
'ctype', 'noruleinit', 'doc', 'name')
def _pop_nonblock(dct):
"""Split a kwargs dictionary into keys meant for Block vs. a _BlockData
subclass.
"""
da = {}
pop_list = [key for key in dct if key not in _block_kwds]
for key in pop_list:
da[key] = dct.pop(key, {})
return da
def _rule_default(b, *args):
"""Default rule for ProcessBlock, which calls build(). A different rule can
be specified to add additional build steps, or to not call build at all
using the normal rule argument to ProcessBlock init.
"""
try:
b.build()
except Exception as e:
logging.getLogger(__name__).exception(
"Failure in build: {}".format(b))
raise e
_process_block_docstring = """
Args:
rule: (Optional) A rule function or None. Default rule calls build().
concrete: If True, make this a toplevel model. **Default** - False.
ctype: (Optional) Pyomo ctype of the Block.\n"""
# These are arguments for Block, but not sure how they should show up in docs
# yet. At this point including these may just be extra confusing.
# options: A dictionary of options
# noruleinit: Don't warn for no-rule initlization of an indexed Block
# doc: Block documentation
# name: Block name\n"""
class _IndexedProcessBlockMeta(type):
"""Metaclass used to create an indexed model class."""
def __new__(meta, name, bases, dct):
def __init__(self, *args, **kwargs):
kwargs.setdefault("rule", _rule_default)
self._block_data_config = _pop_nonblock(kwargs)
bases[0].__init__(self, *args, **kwargs)
dct["__init__"] = __init__
dct["__process_block__"] = "indexed"
return type.__new__(meta, name, bases, dct)
class _ScalarProcessBlockMeta(type):
"""Metaclass used to create a scalar model class."""
def __new__(meta, name, bases, dct):
def __init__(self, *args, **kwargs):
kwargs.setdefault("rule", _rule_default)
self._block_data_config = _pop_nonblock(kwargs)
bases[0].__init__(self, component=self)
bases[1].__init__(self, *args, **kwargs)
dct["__init__"] = __init__
dct["__process_block__"] = "scalar"
return type.__new__(meta, name, bases, dct)
[docs]class ProcessBlock(Block):
"""Process block.
Process block behaves like a Pyomo Block. The important differences are
listed below.
* There is a default rule that calls the build() method for _BlockData
subclass ojects, so subclass of _BlockData used in a ProcessBlock
should have a build() method. A different rule or no rule (None) can be
set with the usual rule argument, if additional steps are required to
build an element of a block. A example of such a case is where
different elements of an indexed block require addtional
information to construct.
* Some of the arguments to __init__, which are not expected arguments of
Block, are split off and stored in self._block_data_config. If the
_BlockData subclass inherits ProcessBlockData, self._block_data_config
is sent to the self.config ConfigBlock.
"""
def __new__(cls, *args, **kwds):
"""Create a new indexed or scalar ProcessBlock subclass instance
depending on whether there are args. If there are args those should be
an indexing set."""
if hasattr(cls, "__process_block__"):
# __process_block__ is a class attribute created when making an
# indexed or scalar subclass of ProcessBlock (or subclass thereof).
# If cls dosen't have it, the indexed or scalar class has not been
# created yet.
#
# You get here after creating a new indexed or scalar class in the
# next if below. The first time in, cls is a ProcessBlock subclass
# that is neither indexed or scalar so you go to the if below and
# create an index or scalar subclass of cls.
return super(Block, cls).__new__(cls)
if args == (): # no args so make scalar class
bname = "_Scalar{}".format(cls.__name__)
n = _ScalarProcessBlockMeta(bname, (cls._ComponentDataClass, cls),{})
return n.__new__(n) #calls this __new__() again with scalar class
else: # args so make indexed class
bname = "_Indexed{}".format(cls.__name__)
n = _IndexedProcessBlockMeta(bname, (cls,), {})
return n.__new__(n) #calls this __new__() again with indexed class
[docs] @classmethod
def base_class_name(cls):
"""Name given by the user to the ProcessBase class.
Return:
(str) Name of the class.
Raises:
AttributeError, if no base class name was set, e.g. this class
was *not* wrapped by the `declare_process_block_class`
decorator.
"""
return cls._orig_name
[docs] @classmethod
def base_class_module(cls):
"""Return module of the associated ProcessBase class.
Return:
(str) Module of the class.
Raises:
AttributeError, if no base class module was set, e.g. this class
was *not* wrapped by the `declare_process_block_class` decorator.
"""
return cls._orig_module
[docs]def declare_process_block_class(name, block_class=ProcessBlock, doc=""):
"""Declare a new ProcessBlock subclass.
This is a decorator function for a class definition, where the class is
derived from _BlockData. It creates a ProcessBlock subclass to contain it.
For example (where ProcessBlockData is a subclass of _BlockData):
@declare_process_block_class(name=MyUnitBlock)
class MyUnitBlockData(ProcessBlockData):
# This class is a _BlockData subclass contained in a Block subclass
# MyUnitBlock
....
The only requirment is that the subclass of _BlockData contain a build()
method.
Args:
name: class name for the model.
block_class: ProcessBlock or a subclass of ProcessBlock, this allows
you to use a subclass of ProcessBlock if needed.
doc: Documentation for the class. This should play nice with sphinx.
"""
def proc_dec(cls): # Decorator function
# create a new class called name from block_class
try:
cb_doc = cls.CONFIG.generate_documentation(
block_start="", block_end="", item_start="%s: ",
indent_spacing=4, item_body="%s", item_end="")
#cb_doc = '\n'.join(' '*4+x for x in cb_doc.splitlines())
except:
cb_doc = ""
ds = "{}\n{}{}\nReturns:\n New {} instance"\
.format(doc, _process_block_docstring, cb_doc, name)
c = type(name, (block_class,),
{"__module__": cls.__module__,
"_ComponentDataClass": cls,
"__doc__":ds})
setattr(sys.modules[cls.__module__], name, c)
setattr(cls, '_orig_name', name)
setattr(cls, '_orig_module', cls.__module__)
return cls
return proc_dec # return decorator function