class IsentropicPerformanceCurveData(ProcessBlockData):
    """Block that holds performance curves. Typically these are in the form of
    constraints that relate head, efficiency, or pressure ratio to volumetric
    or mass flow.  Additional varaibles can be included if needed, such as
    speed. For convenience an option is provided to add head expressions to the
    block. performance curves, and any additional variables, constraints, or
    expressions can be added to this block either via callback provided to the
    configuration, or after the model is constructued."""

    CONFIG = ProcessBlockData.CONFIG(
        doc="Configuration dictionary for the performance curve block.")
    CONFIG.declare("build_callback", ConfigValue(
        default=None,
        doc="Optional callback to add performance curve constraints"))
    CONFIG.declare("build_head_expressions", ConfigValue(
        default=True,
        domain=bool,
        doc="If true add expressions for 'head' and 'head_isentropic'."
            " These expressions can be used in performance curve constraints."))

    def has_constraints(self):
        for o in self.component_data_objects(Constraint):
            return True
        return False

    def build(self):
        super().build()
        if self.config.build_head_expressions:
            try:
                @self.Expression(self.flowsheet().config.time)
                def head_isentropic(b, t): # units are energy/mass
                    b = b.parent_block()
                    if hasattr(b.control_volume.properties_in[t], "flow_mass"):
                        return (b.work_isentropic[t] /
                            b.control_volume.properties_in[t].flow_mass)
                    else:
                        return (b.work_isentropic[t] /
                            b.control_volume.properties_in[t].flow_mol /
                            b.control_volume.properties_in[t].mw)

                @self.Expression(self.flowsheet().config.time)
                def head(b, t): # units are energy/mass
                    b = b.parent_block()
                    if hasattr(b.control_volume.properties_in[t], "flow_mass"):
                        return (b.work_mechanical[t] /
                            b.control_volume.properties_in[t].flow_mass)
                    else:
                        return (b.work_mechanical[t] /
                            b.control_volume.properties_in[t].flow_mol /
                            b.control_volume.properties_in[t].mw)

            except PropertyNotSupportedError:
                _log.exception(
                    "flow_mass or flow_mol and mw are not supported by the"
                    " property package but are required for isentropic pressure"
                    " changer head calculation")
                raise

        if self.config.build_callback is not None:
            self.config.build_callback(self)
Exemple #2
0
class PhysicalParameterBlock(ProcessBlockData,
                             property_meta.HasPropertyClassMetadata):
    """
        This is the base class for thermophysical parameter blocks. These are
        blocks that contain a set of parameters associated with a specific
        thermophysical property package, and are linked to by all instances of
        that property package.
    """
    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "default_arguments",
        ConfigBlock(
            implicit=True,
            description="Default arguments to use with Property Package"))

    def build(self):
        """
        General build method for PropertyParameterBlocks. Inheriting models
        should call super().build.

        Args:
            None

        Returns:
            None
        """
        super(PhysicalParameterBlock, self).build()
Exemple #3
0
class PhysicalParameterBlock(ProcessBlockData,
                             property_meta.HasPropertyClassMetadata):
    """
        This is the base class for thermophysical parameter blocks. These are
        blocks that contain a set of parameters associated with a specific
        thermophysical property package, and are linked to by all instances of
        that property package.
    """
    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "default_arguments",
        ConfigBlock(
            implicit=True,
            description="Default arguments to use with Property Package"))

    def build(self):
        """
        General build method for PropertyParameterBlocks. Inheriting models
        should call super().build.

        Args:
            None

        Returns:
            None
        """
        super(PhysicalParameterBlock, self).build()

    def get_phase_component_set(self):
        """
        Method to get phase-component set for property package. If a phase-
        component set has not been constructed yet, this method will construct
        one.

        Args:
            None

        Returns:
            Phase-component Set object
        """
        try:
            return self._phase_component_set
        except AttributeError:
            # Phase-component set does not exist, so create one.
            pc_set = []
            if hasattr(self.config, "phase_component_list"):
                for p in self.config.phase_component_list:
                    for j in self.config.phase_component_list[p]:
                        pc_set.append((p, j))
            else:
                # Otherwise assume all components in all phases
                for p in self.phase_list:
                    for j in self.component_list:
                        pc_set.append((p, j))

            self._phase_component_set = Set(initialize=pc_set, ordered=True)

            return self._phase_component_set
Exemple #4
0
class MyBlockData(ProcessBlockData):
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare("xinit", ConfigValue(default=1001, domain=float))
    CONFIG.declare("yinit", ConfigValue(default=1002, domain=float))
    def build(self):
        super(MyBlockData, self).build()
        self.x = Var(initialize=self.config.xinit)
        self.y = Var(initialize=self.config.yinit)
Exemple #5
0
class FlowsheetBlockData(ProcessBlockData):
    """
    The FlowsheetBlockData Class forms the base class for all IDAES process
    flowsheet models. The main purpose of this class is to automate the tasks
    common to all flowsheet models and ensure that the necessary attributes of
    a flowsheet model are present.

    The most signfiicant role of the FlowsheetBlockData class is to
    automatically create the time domain for the flowsheet.
    """

    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "dynamic",
        ConfigValue(default=useDefault,
                    domain=In([useDefault, True, False]),
                    description="Dynamic model flag",
                    doc="""Indicates whether this model will be dynamic,
**default** - useDefault.
**Valid values:** {
**useDefault** - get flag from parent or False,
**True** - set as a dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare(
        "time",
        ConfigValue(
            default=None,
            domain=is_time_domain,
            description="Flowsheet time domain",
            doc=
            """Pointer to the time domain for the flowsheet. Users may provide
an existing time domain from another flowsheet, otherwise the flowsheet will
search for a parent with a time domain or create a new time domain and
reference it here."""))
    CONFIG.declare(
        "time_set",
        ConfigValue(
            default=[0],
            domain=list_of_floats,
            description="Set of points for initializing time domain",
            doc="""Set of points for initializing time domain. This should be a
list of floating point numbers,
**default** - [0]."""))
    CONFIG.declare(
        "default_property_package",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Default property package to use in flowsheet",
            doc="""Indicates the default property package to be used by models
within this flowsheet if not otherwise specified,
**default** - None.
**Valid values:** {
**None** - no default property package,
**a ParameterBlock object**.}"""))

    def build(self):
        """
        General build method for FlowsheetBlockData. This method calls a number
        of sub-methods which automate the construction of expected attributes
        of flowsheets.

        Inheriting models should call `super().build`.

        Args:
            None

        Returns:
            None
        """
        super(FlowsheetBlockData, self).build()

        # Set up dynamic flag and time domain
        self._setup_dynamics()

    def is_flowsheet(self):
        """
        Method which returns True to indicate that this component is a
        flowsheet.

        Args:
            None

        Returns:
            True
        """
        return True

    # TODO [Qi]: this should be implemented as a transformation
    def model_check(self):
        """
        This method runs model checks on all unit models in a flowsheet.

        This method searches for objects which inherit from UnitModelBlockData
        and executes the model_check method if it exists.

        Args:
            None

        Returns:
            None
        """
        _log.info("Executing model checks.")
        for o in self.component_objects(descend_into=False):
            if isinstance(o, UnitModelBlockData):
                try:
                    o.model_check()
                except AttributeError:
                    _log.warning('{} Model/block has no model check. To '
                                 'correct this, add a model_check method to '
                                 'the associated unit model class'.format(
                                     o.name))

    def stream_table(self, true_state=False, time_point=0, orient='columns'):
        """
        Method to generate a stream table by iterating over all Arcs in the
        flowsheet.

        Args:
            true_state : whether the state variables (True) or display
                         variables (False, default) from the StateBlocks should
                         be used in the stream table.
            time_point : point in the time domain at which to create stream
                         table (default = 0)
            orient : whether stream should be shown by columns ("columns") or
                     rows ("index")

        Returns:
            A pandas dataframe containing stream table information
        """
        dict_arcs = {}

        for a in self.component_objects(ctype=Arc, descend_into=False):
            dict_arcs[a.local_name] = a

        return create_stream_table_dataframe(dict_arcs,
                                             time_point=time_point,
                                             orient=orient,
                                             true_state=true_state)

    def serialize(self, file_base_name, overwrite=False):
        """
        Serializes the flowsheet and saves it to a file that can be read by the
        idaes-model-vis  jupyter lab extension.

        :param file_base_name: The file prefix to the .idaes.vis file produced.
        The file is created/saved
        in the directory that you ran from Jupyter Lab.
        :param overwrite: Boolean to overwrite an existing file_base_name.idaes.vis.
        If True, the existing file with the same file_base_name will be overwritten.
        This will cause you to lose any saved layout.
        If False and there is an existing file with that file_base_name, you will get
        an error message stating that you cannot save a file to the file_base_name
        (and therefore overwriting the saved layout). If there is not an existing
        file with that file_base_name then it saves as normal.
        Defaults to False.
        :return: None
        """
        serializer = FlowsheetSerializer()
        serializer.serialize(self, file_base_name, overwrite)

    def get_costing(self, module=costing, year=None):
        self.costing = pe.Block()

        module.global_costing_parameters(self.costing, year)

    def _get_stream_table_contents(self, time_point=0):
        """
        Calls stream_table method and returns result
        """
        return self.stream_table(time_point)

    def _setup_dynamics(self):
        # Look for parent flowsheet
        fs = self.flowsheet()

        # Check the dynamic flag, and retrieve if necessary
        if self.config.dynamic == useDefault:
            if fs is None:
                # No parent, so default to steady-state and warn user
                _log.warning('{} is a top level flowsheet, but dynamic flag '
                             'set to useDefault. Dynamic '
                             'flag set to False by default'.format(self.name))
                self.config.dynamic = False

            else:
                # Get dynamic flag from parent flowsheet
                self.config.dynamic = fs.config.dynamic

        # Check for case when dynamic=True, but parent dynamic=False
        elif self.config.dynamic is True:
            if fs is not None and fs.config.dynamic is False:
                raise DynamicError(
                    '{} 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))

        if self.config.time is not None:
            # Validate user provided time domain
            if (self.config.dynamic is True
                    and not isinstance(self.config.time, ContinuousSet)):
                raise DynamicError(
                    '{} was set as a dynamic flowsheet, but time domain '
                    'provided was not a ContinuousSet.'.format(self.name))
        else:
            # If no parent flowsheet, set up time domain
            if fs is None:
                # Create time domain
                if self.config.dynamic:
                    # Check if time_set has at least two points
                    if len(self.config.time_set) < 2:
                        # Check if time_set is at default value
                        if self.config.time_set == [0.0]:
                            # If default, set default end point to be 1.0
                            self.config.time_set = [0.0, 1.0]
                        else:
                            # Invalid user input, raise Excpetion
                            raise DynamicError(
                                "Flowsheet provided with invalid "
                                "time_set attribute - must have at "
                                "least two values (start and end).")
                    # For dynamics, need a ContinuousSet
                    self.time = ContinuousSet(initialize=self.config.time_set)
                else:
                    # For steady-state, use an ordered Set
                    self.time = pe.Set(initialize=self.config.time_set,
                                       ordered=True)

                # Set time config argument as reference to time domain
                self.config.time = self.time
            else:
                # Set time config argument to parent time
                self.config.time = fs.config.time
Exemple #6
0
class PhysicalParameterBlock(ProcessBlockData,
                             property_meta.HasPropertyClassMetadata):
    """
        This is the base class for thermophysical parameter blocks. These are
        blocks that contain a set of parameters associated with a specific
        thermophysical property package, and are linked to by all instances of
        that property package.
    """
    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "default_arguments",
        ConfigBlock(
            implicit=True,
            description="Default arguments to use with Property Package"))

    def build(self):
        """
        General build method for PropertyParameterBlocks. Inheriting models
        should call super().build.

        Args:
            None

        Returns:
            None
        """
        super(PhysicalParameterBlock, self).build()

        # Need this to work with the Helmholtz EoS package
        if not hasattr(self, "_state_block_class"):
            self._state_block_class = None

        # By default, property packages do not include inherent reactions
        self._has_inherent_reactions = False

        # This is a dict to store default property scaling factors. They are
        # defined in the parameter block to provide a universal default for
        # quantities in a particular kind of state block.  For example, you can
        # set flow scaling once instead of for every state block. Some of these
        # may be left for the user to set and some may be defined in a property
        # module where reasonable defaults can be defined a priori. See
        # set_default_scaling, get_default_scaling, and unset_default_scaling
        self.default_scaling_factor = {}

    def set_default_scaling(self, attrbute, value, index=None):
        """Set a default scaling factor for a property.

        Args:
            attribute: property attribute name
            value: default scaling factor
            index: for indexed properties, if this is not provied the scaling
                factor default applies to all indexed elements where specific
                indexes are no specifcally specified.

        Returns:
            None
        """
        self.default_scaling_factor[(attrbute, index)] = value

    def unset_default_scaling(self, attrbute, index=None):
        """Remove a previously set default value

        Args:
            attribute: property attribute name
            index: optional index for indexed properties

        Returns:
            None
        """
        try:
            del self.default_scaling_factor[(attrbute, index)]
        except KeyError:
            pass

    def get_default_scaling(self, attrbute, index=None):
        """ Returns a default scale factor for a property

        Args:
            attribute: property attribute name
            index: optional index for indexed properties

        Returns:
            None
        """
        try:
            # If a specific component data index exists
            return self.default_scaling_factor[(attrbute, index)]
        except KeyError:
            try:
                # indexed, but no specifc index?
                return self.default_scaling_factor[(attrbute, None)]
            except KeyError:
                # Can't find a default scale factor for what you asked for
                return None

    @property
    def state_block_class(self):
        if self._state_block_class is not None:
            return self._state_block_class
        else:
            raise AttributeError(
                "{} has not assigned a StateBlock class to be associated "
                "with this property package. Please contact the developer of "
                "the property package.".format(self.name))

    @property
    def has_inherent_reactions(self):
        return self._has_inherent_reactions

    def build_state_block(self, *args, **kwargs):
        """
        Methods to construct a StateBlock assoicated with this
        PhysicalParameterBlock. This will automatically set the parameters
        construction argument for the StateBlock.

        Returns:
            StateBlock

        """
        default = kwargs.pop("default", {})
        initialize = kwargs.pop("initialize", {})

        if initialize == {}:
            default["parameters"] = self
        else:
            for i in initialize.keys():
                initialize[i]["parameters"] = self

        return self.state_block_class(  # pylint: disable=not-callable
            *args,
            **kwargs,
            default=default,
            initialize=initialize)

    def get_phase_component_set(self):
        """
        Method to get phase-component set for property package. If a phase-
        component set has not been constructed yet, this method will construct
        one.

        Args:
            None

        Returns:
            Phase-Component Set object
        """
        try:
            return self._phase_component_set
        except AttributeError:
            # Phase-component set does not exist, so create one.
            pc_set = []
            for p in self.phase_list:
                p_obj = self.get_phase(p)
                if p_obj.config.component_list is not None:
                    c_list = p_obj.config.component_list
                else:
                    c_list = self.component_list
                for j in c_list:
                    pc_set.append((p, j))

            self._phase_component_set = Set(initialize=pc_set, ordered=True)

            return self._phase_component_set

    def get_component(self, comp):
        """
        Method to retrieve a Component object based on a name from the
        component_list.

        Args:
            comp: name of Component object to retrieve

        Returns:
            Component object
        """
        obj = getattr(self, comp)
        if not isinstance(obj, ComponentData):
            raise PropertyPackageError(
                "{} get_component found an attribute {}, but it does not "
                "appear to be an instance of a Component object.".format(
                    self.name, comp))
        return obj

    def get_phase(self, phase):
        """
        Method to retrieve a Phase object based on a name from the phase_list.

        Args:
            phase: name of Phase object to retrieve

        Returns:
            Phase object
        """
        obj = getattr(self, phase)
        if not isinstance(obj, PhaseData):
            raise PropertyPackageError(
                "{} get_phase found an attribute {}, but it does not "
                "appear to be an instance of a Phase object.".format(
                    self.name, phase))
        return obj

    def _validate_parameter_block(self):
        """
        Tries to catch some possible mistakes and provide the user with
        useful error messages.
        """
        try:
            # Check names in component list have matching Component objects
            for c in self.component_list:
                obj = getattr(self, str(c))
                if not isinstance(obj, ComponentData):
                    raise TypeError(
                        "Property package {} has an object {} whose "
                        "name appears in component_list but is not an "
                        "instance of Component".format(self.name, c))
        except AttributeError:
            # No component list
            raise PropertyPackageError(
                f"Property package {self.name} has not defined any "
                f"Components.")

        try:
            # Valdiate that names in phase list have matching Phase objects
            for p in self.phase_list:
                obj = getattr(self, str(p))
                if not isinstance(obj, PhaseData):
                    raise TypeError(
                        "Property package {} has an object {} whose "
                        "name appears in phase_list but is not an "
                        "instance of Phase".format(self.name, p))
        except AttributeError:
            # No phase list
            raise PropertyPackageError(
                f"Property package {self.name} has not defined any Phases.")

        # Also check that the phase-component set has been created.
        self.get_phase_component_set()
Exemple #7
0
class StateBlockData(ProcessBlockData):
    """
        This is the base class for state block data objects. These are
        blocks that contain the Pyomo components associated with calculating a
        set of thermophysical and transport properties for a given material.
    """
    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "parameters",
        ConfigValue(
            domain=is_physical_parameter_block,
            description="""A reference to an instance of the Property Parameter
Block associated with this property package."""))
    CONFIG.declare(
        "defined_state",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Flag indicating if incoming state is fully defined",
            doc="""Flag indicating whether the state should be considered fully
defined, and thus whether constraints such as sum of mass/mole fractions should
be included,
**default** - False.
**Valid values:** {
**True** - state variables will be fully defined,
**False** - state variables will not be fully defined.}"""))
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=True,
            domain=Bool,
            description="Phase equilibrium constraint flag",
            doc="""Flag indicating whether phase equilibrium constraints
should be constructed in this state block,
**default** - True.
**Valid values:** {
**True** - StateBlock should calculate phase equilibrium,
**False** - StateBlock should not calculate phase equilibrium.}"""))

    def __init__(self, *args, **kwargs):
        self._lock_attribute_creation = False
        super().__init__(*args, **kwargs)

    def lock_attribute_creation_context(self):
        """Returns a context manager that does not allow attributes to be created
        while in the context and allows attributes to be created normally outside
        the context.
        """
        return _lock_attribute_creation_context(self)

    def is_property_constructed(self, attr):
        """Returns True if the attribute ``attr`` already exists, or false if it
        would be added in ``__getattr__``, or does not exist.

        Args:
            attr (str): Attribute name to check

        Return:
            True if the attribute is already constructed, False otherwise
        """
        with self.lock_attribute_creation_context():
            return hasattr(self, attr)

    @property
    def component_list(self):
        return self.parent_component()._return_component_list()

    @property
    def phase_list(self):
        return self.parent_component()._return_phase_list()

    @property
    def phase_component_set(self):
        return self.parent_component()._return_phase_component_set()

    @property
    def has_inherent_reactions(self):
        return self.parent_component()._has_inherent_reactions()

    @property
    def include_inherent_reactions(self):
        return self.parent_component()._include_inherent_reactions()

    def build(self):
        """
        General build method for StateBlockDatas.

        Args:
            None

        Returns:
            None
        """
        super(StateBlockData, self).build()
        add_object_reference(self, "_params", self.config.parameters)

        # TODO: Deprecate this at some point
        # Backwards compatability check for old-style property packages
        self._params._validate_parameter_block()

    @property
    def params(self):
        return self._params

    def define_state_vars(self):
        """
        Method that returns a dictionary of state variables used in property
        package. Implement a placeholder method which returns an Exception to
        force users to overload this.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' define_state_vars method. Please contact '
                                  'the property package developer.')

    def define_port_members(self):
        """
        Method used to specify components to populate Ports with. Defaults to
        define_state_vars, and developers should overload as required.
        """
        return self.define_state_vars()

    def define_display_vars(self):
        """
        Method used to specify components to use to generate stream tables and
        other outputs. Defaults to define_state_vars, and developers should
        overload as required.
        """
        return self.define_state_vars()

    def get_material_flow_terms(self, *args, **kwargs):
        """
        Method which returns a valid expression for material flow to use in
        the material balances.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' get_material_flow_terms method. Please '
                                  'contact the property package developer.')

    def get_material_density_terms(self, *args, **kwargs):
        """
        Method which returns a valid expression for material density to use in
        the material balances .
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' get_material_density_terms method. Please '
                                  'contact the property package developer.')

    def get_material_diffusion_terms(self, *args, **kwargs):
        """
        Method which returns a valid expression for material diffusion to use
        in the material balances.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' get_material_diffusion_terms method. '
                                  'Please contact the property package '
                                  'developer.')

    def get_enthalpy_flow_terms(self, *args, **kwargs):
        """
        Method which returns a valid expression for enthalpy flow to use in
        the energy balances.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' get_enthalpy_flow_terms method. Please '
                                  'contact the property package developer.')

    def get_energy_density_terms(self, *args, **kwargs):
        """
        Method which returns a valid expression for enthalpy density to use in
        the energy balances.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' get_energy_density_terms method. Please '
                                  'contact the property package developer.')

    def get_energy_diffusion_terms(self, *args, **kwargs):
        """
        Method which returns a valid expression for energy diffusion to use in
        the energy balances.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' get_energy_diffusion_terms method. '
                                  'Please contact the property package '
                                  'developer.')

    def get_material_flow_basis(self, *args, **kwargs):
        """
        Method which returns an Enum indicating the basis of the material flow
        term.
        """
        return MaterialFlowBasis.other

    def calculate_bubble_point_temperature(self, *args, **kwargs):
        """
        Method which computes the bubble point temperature for a multi-
        component mixture given a pressure and mole fraction.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' calculate_bubble_point_temperature method.'
                                  ' Please contact the property package '
                                  'developer.')

    def calculate_dew_point_temperature(self, *args, **kwargs):
        """
        Method which computes the dew point temperature for a multi-
        component mixture given a pressure and mole fraction.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' calculate_dew_point_temperature method.'
                                  ' Please contact the property package '
                                  'developer.')

    def calculate_bubble_point_pressure(self, *args, **kwargs):
        """
        Method which computes the bubble point pressure for a multi-
        component mixture given a temperature and mole fraction.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' calculate_bubble_point_pressure method.'
                                  ' Please contact the property package '
                                  'developer.')

    def calculate_dew_point_pressure(self, *args, **kwargs):
        """
        Method which computes the dew point pressure for a multi-
        component mixture given a temperature and mole fraction.
        """
        raise NotImplementedError('{} property package has not implemented the'
                                  ' calculate_dew_point_pressure method.'
                                  ' Please contact the property package '
                                  'developer.')

    def __getattr__(self, attr):
        """
        This method is used to avoid generating unnecessary property
        calculations in state 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.
        """
        if self._lock_attribute_creation:
            raise AttributeError(
                f"{attr} does not exist, and attribute creation is locked.")

        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 PropertyPackageError(
                    "{} 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 PropertyPackageError(
                '{} {} does not exist, but is a protected '
                'attribute. Check the naming of your '
                'components to avoid any reserved names'.format(
                    self.name, attr))

        if attr == "config":
            try:
                self._get_config_args()
                return self.config
            except:
                raise BurntToast("{} getattr method was triggered by a call "
                                 "to the config block, but _get_config_args "
                                 "failed. This should never happen.")

        # Check for recursive calls
        try:
            # Check if __getattrcalls is initialized
            self.__getattrcalls
        except AttributeError:
            # Initialize it
            self.__getattrcalls = [attr]
        else:
            # 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 PropertyPackageError(
                        '{} _{} 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 PropertyPackageError(
                        '{} 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)

        # Get property information from properties metadata
        try:
            m = self.config.parameters.get_metadata().properties

            if m is None:
                raise PropertyPackageError(
                    '{} property package get_metadata()'
                    ' 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 metadata, assume package does not
            # support property
            clear_call_list(self, attr)
            raise PropertyNotSupportedError(
                '{} {} is not supported by property package (property is '
                'not listed in package metadata properties).'.format(
                    self.name, attr))

        # Get method name from resulting properties
        try:
            if m[attr]['method'] is None:
                # If method is none, property should be constructed
                # by property package, so raise PropertyPackageError
                clear_call_list(self, attr)
                raise PropertyPackageError(
                    '{} {} 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 PropertyNotSupportedError(
                    '{} {} is not supported by property package '
                    '(property method is listed as False in '
                    'package property metadata).'.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 PropertyPackageError(
                        '{} {} package property metadata 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 PropertyPackageError(
                    '{} {} package property metadata 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 PropertyNotSupportedError(
                '{} package property metadata method '
                'does not contain a method for {}. '
                'Please select a package which supports '
                'the necessary properties for your process.'.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 Exception:
                # 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 PropertyPackageError(
                '{} tried calling attribute {} in order to create '
                'component {}. However the method is not callable.'.format(
                    self.name, f, attr))

        # Clear call list, and return
        comp = getattr(self, attr)
        clear_call_list(self, attr)
        return comp

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        # Get scaling factor defaults, if no scaling factor set
        for v in self.component_data_objects((Constraint, Var, Expression),
                                             descend_into=False):
            if iscale.get_scaling_factor(v) is None:  # don't replace if set
                name = v.getname().split("[")[0]
                index = v.index()
                sf = self.config.parameters.get_default_scaling(name, index)
                if sf is not None:
                    iscale.set_scaling_factor(v, sf)
Exemple #8
0
class PIDBlockData(ProcessBlockData):
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "pv",
        ConfigValue(
            default=None,
            description="Measured process variable",
            doc="A Pyomo Var, Expression, or Reference for the measured"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "output",
        ConfigValue(
            default=None,
            description="Controlled process variable",
            doc="A Pyomo Var, Expression, or Reference for the controlled"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "upper",
        ConfigValue(
            default=1.0,
            domain=float,
            description="Output upper limit",
            doc="The upper limit for the controller output, default=1"))
    CONFIG.declare(
        "lower",
        ConfigValue(
            default=0.0,
            domain=float,
            description="Output lower limit",
            doc="The lower limit for the controller output, default=0"))
    CONFIG.declare(
        "calculate_initial_integral",
        ConfigValue(
            default=True,
            domain=bool,
            description="Calculate the initial integral term value if true, "
            " otherwise provide a variable err_i0, which can be fixed",
            doc="Calculate the initial integral term value if true, otherwise"
            " provide a variable err_i0, which can be fixed, default=True"))
    CONFIG.declare(
        "pid_form",
        ConfigValue(default=PIDForm.velocity,
                    domain=In(PIDForm),
                    description="Velocity or standard form",
                    doc="Velocity or standard form"))

    # TODO<jce> options for P, PI, and PD, you can currently do PI by setting
    #           the derivative time to 0, this class should handle PI and PID
    #           controllers. Proportional, only controllers are sufficiently
    #           different that another class should be implemented.
    # TODO<jce> Anti-windup the integral term can keep accumulating error when
    #           the controller output is at a bound. This can cause trouble,
    #           and ways to deal with it should be implemented
    # TODO<jce> Implement way to better deal with the integral term for setpoint
    #           changes (see bumpless).  I need to look into the more, but this
    #           would basically use the calculation like the one already used
    #           for the first time point to calculate integral error to keep the
    #           controller output from suddenly jumping in response to a set
    #           point change or transition from manual to automatic control.

    def _build_standard(self, time_set, t0):
        # Want to fix the output variable at the first time step to make
        # solving easier. This calculates the initial integral error to line up
        # with the initial output value, keeps the controller from initially
        # jumping.
        if self.config.calculate_initial_integral:

            @self.Expression(doc="Initial integral error")
            def err_i0(b):
                return b.time_i[t0]*(b.output[t0] - b.gain[t0]*b.pterm[t0]\
                       - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0]

        # integral error
        @self.Expression(time_set, doc="Integral error")
        def err_i(b, t_end):
            return b.err_i0 + sum((b.iterm[t] + b.iterm[time_set.prev(t)]) *
                                  (t - time_set.prev(t)) / 2.0
                                  for t in time_set if t <= t_end and t > t0)

        # Calculate the unconstrained controller output
        @self.Expression(time_set, doc="Unconstrained controller output")
        def unconstrained_output(b, t):
            return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i[t] +
                                b.time_d[t] * b.err_d[t])

        @self.Expression(doc="Initial integral error at the end")
        def err_i_end(b):
            return b.err_i[time_set.last()]

    def _build_velocity(self, time_set, t0):
        if self.config.calculate_initial_integral:

            @self.Expression(doc="Initial integral error")
            def err_i0(b):
                return b.time_i[t0]*(b.output[t0] - b.gain[t0]*b.pterm[t0]\
                       - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0]

        # Calculate the unconstrained controller output
        @self.Expression(time_set, doc="Unconstrained controller output")
        def unconstrained_output(b, t):
            if t == t0:  # do the standard first step so I have a previous time
                # for the rest of the velocity form
                return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i0 +
                                    b.time_d[t] * b.err_d[t])
            tb = time_set.prev(t)  # time back a step
            return self.output[tb] + self.gain[t] * (
                b.pterm[t] - b.pterm[tb] + (t - tb) / b.time_i[t] *
                (b.err[t] + b.err[tb]) / 2 + b.time_d[t] *
                (b.err_d[t] - b.err_d[tb]))

        @self.Expression(doc="Initial integral error at the end")
        def err_i_end(b):
            tl = time_set.last()
            return b.time_i[tl]*(b.output[tl] - b.gain[tl]*b.pterm[tl]\
                   - b.gain[tl]*b.time_d[tl]*b.err_d[tl])/b.gain[tl]

    def build(self):
        """
        Build the PID block
        """
        if isinstance(self.flowsheet().time, ContinuousSet):
            # time may not be a continuous set if you have a steady state model
            # in the steady state model case obviously the controller should
            # not be active, but you can still add it.
            if 'scheme' not in self.flowsheet().time.get_discretization_info():
                # if you have a dynamic model, must do time discretization
                # before adding the PID model
                raise RunTimeError(
                    "PIDBlock must be added after time discretization")

        super().build()  # do the ProcessBlockData voodoo for config
        # Check for required config
        if self.config.pv is None:
            raise ConfigurationError("Controller configuration requires 'pv'")
        if self.config.output is None:
            raise ConfigurationError(
                "Controller configuration requires 'output'")
        # Shorter pointers to time set information
        time_set = self.flowsheet().time
        t0 = time_set.first()
        # Variable for basic controller settings may change with time.
        self.setpoint = pyo.Var(time_set, doc="Setpoint")
        self.gain = pyo.Var(time_set, doc="Controller gain")
        self.time_i = pyo.Var(time_set, doc="Integral time")
        self.time_d = pyo.Var(time_set, doc="Derivative time")
        # Make the initial derivative term a variable so you can set it. This
        # should let you carry on from the end of another time period
        self.err_d0 = pyo.Var(doc="Initial derivative term", initialize=0)
        self.err_d0.fix()
        if not self.config.calculate_initial_integral:
            self.err_i0 = pyo.Var(doc="Initial integral term", initialize=0)
            self.err_i0.fix()
        # Make references to the output and measured variables
        self.pv = pyo.Reference(self.config.pv)  # No duplicate
        self.output = pyo.Reference(self.config.output)  # No duplicate
        # Create an expression for error from setpoint
        @self.Expression(time_set, doc="Setpoint error")
        def err(b, t):
            return self.setpoint[t] - self.pv[t]

        # Use expressions to allow the some future configuration
        @self.Expression(time_set)
        def pterm(b, t):
            return -self.pv[t]

        @self.Expression(time_set)
        def dterm(b, t):
            return -self.pv[t]

        @self.Expression(time_set)
        def iterm(b, t):
            return self.err[t]

        # Output limits parameter
        self.limits = pyo.Param(["l", "h"],
                                mutable=True,
                                doc="controller output limits",
                                initialize={
                                    "l": self.config.lower,
                                    "h": self.config.upper
                                })
        # Smooth min and max are used to limit output, smoothing parameter here
        self.smooth_eps = pyo.Param(
            mutable=True,
            initialize=1e-4,
            doc="Smoothing parameter for controller output limits")
        # This is ugly, but want integral and derivative error as expressions,
        # nice implementation with variables is harder to initialize and solve
        @self.Expression(time_set, doc="Derivative error.")
        def err_d(b, t):
            if t == t0:
                return self.err_d0
            else:
                return (b.dterm[t] - b.dterm[time_set.prev(t)])\
                       /(t - time_set.prev(t))

        if self.config.pid_form == PIDForm.standard:
            self._build_standard(time_set, t0)
        else:
            self._build_velocity(time_set, t0)

        # Add the controller output constraint and limit it with smooth min/max
        e = self.smooth_eps
        h = self.limits["h"]
        l = self.limits["l"]

        @self.Constraint(time_set, doc="Controller output constraint")
        def output_constraint(b, t):
            if t == t0:
                return pyo.Constraint.Skip
            else:
                return self.output[t] ==\
                    smooth_min(
                        smooth_max(self.unconstrained_output[t], l, e), h, e)
class ReactionBlockDataBase(ProcessBlockData):
    """
        This is the base class for reaction block data objects. These are
        blocks that contain the Pyomo components associated with calculating a
        set of reacion properties for a given material.
    """
    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "parameters",
        ConfigValue(
            domain=is_reaction_parameter_block,
            description="""A reference to an instance of the Reaction Parameter
Block associated with this property package."""))
    CONFIG.declare(
        "state_block",
        ConfigValue(
            domain=is_state_block,
            description="""A reference to an instance of a StateBlock with
which this reaction block should be associated."""))
    CONFIG.declare(
        "has_equilibrium",
        ConfigValue(default=True,
                    domain=In([True, False]),
                    description="Equilibrium constraint flag",
                    doc="""Flag indicating whether equilibrium constraints
should be constructed in this reaction block,
**default** - True.
**Valid values:** {
**True** - ReactionBlock should enforce equilibrium constraints,
**False** - ReactionBlock should not enforce equilibrium constraints.}"""))

    def __init__(self, *args, **kwargs):
        self._lock_attribute_creation = False
        super().__init__(*args, **kwargs)

    def lock_attribute_creation_context(self):
        """Returns a context manager that does not allow attributes to be created
        while in the context and allows attributes to be created normally outside
        the context.
        """
        return _lock_attribute_creation_context(self)

    def is_property_constructed(self, attr):
        """Returns True if the attribute ``attr`` already exists, or false if it
        would be added in ``__getattr__``, or does not exist.

        Args:
            attr (str): Attribute name to check

        Return:
            True if the attribute is already constructed, False otherwise 
        """
        with self.lock_attribute_creation_context():
            return hasattr(self, attr)

    def build(self):
        """
        General build method for PropertyBlockDatas. Inheriting models should
        call super().build.

        Args:
            None

        Returns:
            None
        """
        super(ReactionBlockDataBase, self).build()
        add_object_reference(self, "_params", self.config.parameters)

        self._validate_state_block()

    @property
    def params(self):
        return self._params

    def _validate_state_block(self):
        """
        Method to validate that the associated state block matches with the
        PropertyParameterBlock assoicated with the ReactionParameterBlock.
        """
        # Add a reference to the corresponding state block data for later use
        add_object_reference(self, "state_ref",
                             self.config.state_block[self.index()])

        # Validate that property package of state matches that of reaction pack
        if (self.config.parameters.config.property_package !=
                self.state_ref.config.parameters):
            raise PropertyPackageError(
                '{} the StateBlock associated with this '
                'ReactionBlock does not match with the '
                'PropertyParamterBlock associated with the '
                'ReactionParameterBlock. The modelling framework '
                'does not support mixed associations of property '
                'and reaction packages.'.format(self.name))

    def get_reaction_rate_basis(self):
        """
        Method which returns an Enum indicating the basis of the reaction rate
        term.
        """
        return MaterialFlowBasis.other

    def __getattr__(self, attr):
        """
        This method is used to avoid generating unnecessary property
        calculations in reaction 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.
        """
        if self._lock_attribute_creation:
            raise AttributeError(
                f"{attr} does not exist, and attribute creation is locked.")

        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 PropertyPackageError(
                    "{} 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 PropertyPackageError(
                '{} {} does not exist, but is a protected '
                'attribute. Check the naming of your '
                'components to avoid any reserved names'.format(
                    self.name, attr))

        if attr == "config":
            try:
                self._get_config_args()
                return self.config
            except:
                raise BurntToast("{} getattr method was triggered by a call "
                                 "to the config block, but _get_config_args "
                                 "failed. This should never happen.")

        # Check for recursive calls
        try:
            # Check if __getattrcalls is initialized
            self.__getattrcalls
        except AttributeError:
            # Initialize it
            self.__getattrcalls = [attr]
        else:
            # 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 PropertyPackageError(
                        '{} _{} 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 PropertyPackageError(
                        '{} 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)

        # Get property information from get_supported_properties
        try:
            m = self.config.parameters.get_metadata().properties

            if m is None:
                raise PropertyPackageError(
                    '{} reaction 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 PropertyNotSupportedError(
                '{} {} is not supported by reaction 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 PropertyPackageError
                clear_call_list(self, attr)
                raise PropertyPackageError(
                    '{} {} should be constructed automatically '
                    'by reaction 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 PropertyNotSupportedError(
                    '{} {} is not supported by reaction 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 PropertyPackageError(
                        '{} {} get_supported_properties method '
                        'returned a name that does not correspond'
                        ' to any method in the reaction package. '
                        'Please contact the developer of the '
                        'reaction package.'.format(self.name, attr))
            else:
                # Otherwise method name is invalid
                clear_call_list(self, attr)
                raise PropertyPackageError(
                    '{} {} get_supported_properties method '
                    'returned invalid value for method name. '
                    'Please contact the developer of the '
                    'reaction 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 PropertyNotSupportedError(
                '{} get_supported_properties method '
                'does not contain a method for {}. '
                'Please select a package which supports '
                'the necessary properties for your process.'.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 Exception:
                # 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 PropertyPackageError(
                '{} tried calling attribute {} in order to create '
                'component {}. However the method is not callable.'.format(
                    self.name, f, attr))

        # Clear call list, and return
        comp = getattr(self, attr)
        clear_call_list(self, attr)
        return comp
Exemple #10
0
class FlowsheetBlockData(ProcessBlockData):
    """
    The FlowsheetBlockData Class forms the base class for all IDAES process
    flowsheet models. The main purpose of this class is to automate the tasks
    common to all flowsheet models and ensure that the necessary attributes of
    a flowsheet model are present.

    The most signfiicant role of the FlowsheetBlockData class is to
    automatically create the time domain for the flowsheet.
    """

    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare("dynamic", ConfigValue(
        default=useDefault,
        domain=In([useDefault, True, False]),
        description="Dynamic model flag",
        doc="""Indicates whether this model will be dynamic,
**default** - useDefault.
**Valid values:** {
**useDefault** - get flag from parent or False,
**True** - set as a dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare("time", ConfigValue(
        default=None,
        domain=is_time_domain,
        description="Flowsheet time domain",
        doc="""Pointer to the time domain for the flowsheet. Users may provide
an existing time domain from another flowsheet, otherwise the flowsheet will
search for a parent with a time domain or create a new time domain and
reference it here."""))
    CONFIG.declare("time_set", ConfigValue(
        default=[0],
        domain=list_of_floats,
        description="Set of points for initializing time domain",
        doc="""Set of points for initializing time domain. This should be a
list of floating point numbers,
**default** - [0]."""))
    CONFIG.declare("default_property_package", ConfigValue(
        default=None,
        domain=is_physical_parameter_block,
        description="Default property package to use in flowsheet",
        doc="""Indicates the default property package to be used by models
within this flowsheet if not otherwise specified,
**default** - None.
**Valid values:** {
**None** - no default property package,
**a ParameterBlock object**.}"""))

    def build(self):
        """
        General build method for FlowsheetBlockData. This method calls a number
        of sub-methods which automate the construction of expected attributes
        of flowsheets.

        Inheriting models should call `super().build`.

        Args:
            None

        Returns:
            None
        """
        super(FlowsheetBlockData, self).build()

        # Set up dynamic flag and time domain
        self._setup_dynamics()

    def is_flowsheet(self):
        """
        Method which returns True to indicate that this component is a
        flowsheet.

        Args:
            None

        Returns:
            True
        """
        return True

    # TODO [Qi]: this should be implemented as a transformation
    def model_check(self):
        """
        This method runs model checks on all unit models in a flowsheet.

        This method searches for objects which inherit from UnitModelBlockData
        and executes the model_check method if it exists.

        Args:
            None

        Returns:
            None
        """
        _log.info("Executing model checks.")
        for o in self.component_objects(descend_into=False):
            if isinstance(o, UnitModelBlockData):
                try:
                    o.model_check()
                except AttributeError:
                    _log.warning('{} Model/block has no model check. To '
                                 'correct this, add a model_check method to '
                                 'the associated unit model class'
                                 .format(o.name))

    def _setup_dynamics(self):
        # Look for parent flowsheet
        fs = self.flowsheet()

        # Check the dynamic flag, and retrieve if necessary
        if self.config.dynamic == useDefault:
            if fs is None:
                # No parent, so default to steady-state and warn user
                _log.warning('{} is a top level flowhseet, but dynamic flag '
                             'set to useDefault. Dynamic '
                             'flag set to False by default'
                             .format(self.name))
                self.config.dynamic = False

            else:
                # Get dynamic flag from parent flowsheet
                self.config.dynamic = fs.config.dynamic

        # Check for case when dynamic=True, but parent dynamic=False
        elif self.config.dynamic is True:
            if fs is not None and fs.config.dynamic is False:
                raise DynamicError(
                        '{} 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))

        if self.config.time is not None:
            # Validate user provided time domain
            if (self.config.dynamic is True and
                    not isinstance(self.config.time, ContinuousSet)):
                raise DynamicError(
                        '{} was set as a dynamic flowsheet, but time domain '
                        'provided was not a ContinuousSet.'.format(self.name))
        else:
            # If no parent flowsheet, set up time domain
            if fs is None:
                # Create time domain
                if self.config.dynamic:
                    # Check if time_set has at least two points
                    if len(self.config.time_set) < 2:
                        # Check if time_set is at default value
                        if self.config.time_set == [0.0]:
                            # If default, set default end point to be 1.0
                            self.config.time_set = [0.0, 1.0]
                        else:
                            # Invalid user input, raise Excpetion
                            raise DynamicError(
                                    "Flowsheet provided with invalid "
                                    "time_set attribute - must have at "
                                    "least two values (start and end).")
                    # For dynamics, need a ContinuousSet
                    self.time = ContinuousSet(initialize=self.config.time_set)
                else:
                    # For steady-state, use an ordered Set
                    self.time = pe.Set(initialize=self.config.time_set,
                                       ordered=True)

                # Set time config argument as reference to time domain
                self.config.time = self.time
            else:
                # Set time config argument to parent time
                self.config.time = fs.config.time
Exemple #11
0
class PIDBlockData(ProcessBlockData):
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "pv",
        ConfigValue(
            default=None,
            description="Measured process variable",
            doc="A Pyomo Var, Expression, or Reference for the measured"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "output",
        ConfigValue(
            default=None,
            description="Controlled process variable",
            doc="A Pyomo Var, Expression, or Reference for the controlled"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "upper",
        ConfigValue(
            default=1.0,
            domain=float,
            description="Output upper limit",
            doc="The upper limit for the controller output, default=1"))
    CONFIG.declare(
        "lower",
        ConfigValue(
            default=0.0,
            domain=float,
            description="Output lower limit",
            doc="The lower limit for the controller output, default=0"))
    CONFIG.declare(
        "calculate_initial_integral",
        ConfigValue(
            default=True,
            domain=bool,
            description="Calculate the initial integral term value if true, "
            " otherwise provide a variable err_i0, which can be fixed",
            doc="Calculate the initial integral term value if true, otherwise"
            " provide a variable err_i0, which can be fixed, default=True"))

    # other options can be added, but this covers the bare minimum
    #
    # TODO<jce> options for P, PI, and PD, you can currently do PI by setting
    #           the derivative time to 0, but it would be better not to
    #           add the derivative term at all if not needed.  PD and P are less
    #           common, but they exist and should be supported
    # TODO<jce> Anti-windup the integral term can keep accumulating error when
    #           the controller output is at a bound. This can cause trouble,
    #           and ways to deal with it should be implemented
    # TODO<jce> Implement way to better deal with the integral term for setpoint
    #           changes (see bumpless).  I need to look into the more, but this
    #           would basically use the calculation like the one already used
    #           for the first time point to calculate integral error to keep the
    #           controller output from suddenly jumping in response to a set
    #           point change or transition from manual to automatic control.
    # TODO<jce> Implement the integral term using variables.  The integral
    #           expressions are nice because initialization is not required and
    #           they reduce the total number of variables, but they there is an
    #           integral expression and each time and the later ones contain a
    #           very large number of terms.

    def build(self):
        """
        Build the PID block
        """
        super().build()  # do the ProcessBlockData voodoo for config
        # Check for required config
        if self.config.pv is None:
            raise ConfigurationError("Controller configuration requires 'pv'")
        if self.config.output is None:
            raise ConfigurationError(
                "Controller configuration requires 'output'")
        # Shorter pointers to time set information
        time_set = self.flowsheet().time
        t0 = time_set.first()
        # Variable for basic controller settings may change with time.
        self.setpoint = pyo.Var(time_set, doc="Setpoint")
        self.gain = pyo.Var(time_set, doc="Controller gain")
        self.time_i = pyo.Var(time_set, doc="Integral time")
        self.time_d = pyo.Var(time_set, doc="Derivative time")
        # Make the initial derivative term a variable so you can set it. This
        # should let you carry on from the end of another time period
        self.err_d0 = pyo.Var(doc="Initial derivative term", initialize=0)
        self.err_d0.fix()
        if not self.config.calculate_initial_integral:
            self.err_i0 = pyo.Var(doc="Initial integral term", initialize=0)
            self.err_i0.fix()
        # Make references to the output and measured variables
        self.pv = pyo.Reference(self.config.pv)  # No duplicate
        self.output = pyo.Reference(self.config.output)  # No duplicate
        # Create an expression for error from setpoint
        @self.Expression(time_set, doc="Setpoint error")
        def err(b, t):
            return self.setpoint[t] - self.pv[t]

        # Use expressions to allow the some future configuration
        @self.Expression(time_set)
        def pterm(b, t):
            return -self.pv[t]

        @self.Expression(time_set)
        def dterm(b, t):
            return -self.pv[t]

        @self.Expression(time_set)
        def iterm(b, t):
            return self.err[t]

        # Output limits parameter
        self.limits = pyo.Param(["l", "h"],
                                mutable=True,
                                doc="controller output limits",
                                initialize={
                                    "l": self.config.lower,
                                    "h": self.config.upper
                                })
        # Smooth min and max are used to limit output, smoothing parameter here
        self.smooth_eps = pyo.Param(
            mutable=True,
            initialize=1e-4,
            doc="Smoothing parameter for controller output limits")
        # This is ugly, but want integral and derivative error as expressions,
        # nice implementation with variables is harder to initialize and solve
        @self.Expression(time_set, doc="Derivative error.")
        def err_d(b, t):
            if t == t0:
                return self.err_d0
            else:
                return (b.dterm[t] - b.dterm[time_set.prev(t)])\
                       /(t - time_set.prev(t))

        # Want to fix the output varaible at the first time step to make
        # solving easier. This calculates the initial integral error to line up
        # with the initial output value, keeps the controller from initially
        # jumping.
        if self.config.calculate_initial_integral:

            @self.Expression(doc="Initial integral error")
            def err_i0(b):
                return b.time_i[t0]*(b.output[0] - b.gain[t0]*b.pterm[t0]\
                       - b.gain[t0]*b.time_d[t0]*b.err_d[t0])/b.gain[t0]

        # integral error
        @self.Expression(time_set, doc="Integral error")
        def err_i(b, t_end):
            return b.err_i0 + sum((b.iterm[t] + b.iterm[time_set.prev(t)]) *
                                  (t - time_set.prev(t)) / 2.0
                                  for t in time_set if t <= t_end and t > t0)

        # Calculate the unconstrainted controller output
        @self.Expression(time_set, doc="Unconstrained contorler output")
        def unconstrained_output(b, t):
            return b.gain[t] * (b.pterm[t] + 1.0 / b.time_i[t] * b.err_i[t] +
                                b.time_d[t] * b.err_d[t])

        # Add the controller output constraint and limit it with smooth min/max
        e = self.smooth_eps
        h = self.limits["h"]
        l = self.limits["l"]

        @self.Constraint(time_set, doc="Controller output constraint")
        def output_constraint(b, t):
            if t == t0:
                return pyo.Constraint.Skip
            else:
                return self.output[t] ==\
                    smooth_min(
                        smooth_max(self.unconstrained_output[t], l, e), h, e)
class FlowsheetBlockData(ProcessBlockData):
    """
    The FlowsheetBlockData Class forms the base class for all IDAES process
    flowsheet models. The main purpose of this class is to automate the tasks
    common to all flowsheet models and ensure that the necessary attributes of
    a flowsheet model are present.

    The most signfiicant role of the FlowsheetBlockData class is to
    automatically create the time domain for the flowsheet.
    """

    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "dynamic",
        ConfigValue(default=useDefault,
                    domain=In([useDefault, True, False]),
                    description="Dynamic model flag",
                    doc="""Indicates whether this model will be dynamic,
**default** - useDefault.
**Valid values:** {
**useDefault** - get flag from parent or False,
**True** - set as a dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare(
        "time",
        ConfigValue(
            default=None,
            domain=is_time_domain,
            description="Flowsheet time domain",
            doc=
            """Pointer to the time domain for the flowsheet. Users may provide
an existing time domain from another flowsheet, otherwise the flowsheet will
search for a parent with a time domain or create a new time domain and
reference it here."""))
    CONFIG.declare(
        "time_set",
        ConfigValue(
            default=[0],
            domain=list_of_floats,
            description="Set of points for initializing time domain",
            doc="""Set of points for initializing time domain. This should be a
list of floating point numbers,
**default** - [0]."""))
    CONFIG.declare(
        "time_units",
        ConfigValue(
            description="Units for time domain",
            doc="""Pyomo Units object describing the units of the time domain.
This must be defined for dynamic simulations, default = None."""))
    CONFIG.declare(
        "default_property_package",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Default property package to use in flowsheet",
            doc="""Indicates the default property package to be used by models
within this flowsheet if not otherwise specified,
**default** - None.
**Valid values:** {
**None** - no default property package,
**a ParameterBlock object**.}"""))

    def build(self):
        """
        General build method for FlowsheetBlockData. This method calls a number
        of sub-methods which automate the construction of expected attributes
        of flowsheets.

        Inheriting models should call `super().build`.

        Args:
            None

        Returns:
            None
        """
        super(FlowsheetBlockData, self).build()

        self._time_units = None

        # Set up dynamic flag and time domain
        self._setup_dynamics()

    @property
    def time_units(self):
        return self._time_units

    def is_flowsheet(self):
        """
        Method which returns True to indicate that this component is a
        flowsheet.

        Args:
            None

        Returns:
            True
        """
        return True

    def model_check(self):
        """
        This method runs model checks on all unit models in a flowsheet.

        This method searches for objects which inherit from UnitModelBlockData
        and executes the model_check method if it exists.

        Args:
            None

        Returns:
            None
        """
        _log.info("Executing model checks.")
        for o in self.component_objects(descend_into=False):
            if isinstance(o, UnitModelBlockData):
                try:
                    o.model_check()
                except AttributeError:
                    # This should never happen, but just in case
                    _log.warning(
                        '{} Model/block has no model_check method.'.format(
                            o.name))

    def stream_table(self, true_state=False, time_point=0, orient='columns'):
        """
        Method to generate a stream table by iterating over all Arcs in the
        flowsheet.

        Args:
            true_state : whether the state variables (True) or display
                         variables (False, default) from the StateBlocks should
                         be used in the stream table.
            time_point : point in the time domain at which to create stream
                         table (default = 0)
            orient : whether stream should be shown by columns ("columns") or
                     rows ("index")

        Returns:
            A pandas dataframe containing stream table information
        """
        dict_arcs = {}

        for a in self.component_objects(ctype=Arc, descend_into=False):
            dict_arcs[a.local_name] = a

        return create_stream_table_dataframe(dict_arcs,
                                             time_point=time_point,
                                             orient=orient,
                                             true_state=true_state)

    def visualize(self, model_name, **kwargs):
        """
        Starts up a flask server that serializes the model and pops up a 
        webpage with the visualization

        Args:
            model_name : The name of the model that flask will use as an argument
                         for the webpage
        Keyword Args:
            **kwargs: Additional keywords for :func:`idaes.ui.fsvis.visualize()`

        Returns:
            None
        """
        visualize(self, model_name, **kwargs)

    def get_costing(self, module=costing, year=None):
        self.costing = pe.Block()

        module.global_costing_parameters(self.costing, year)

    def _get_stream_table_contents(self, time_point=0):
        """
        Calls stream_table method and returns result
        """
        return self.stream_table(time_point)

    def _setup_dynamics(self):
        # Look for parent flowsheet
        fs = self.flowsheet()

        # Check the dynamic flag, and retrieve if necessary
        if self.config.dynamic == useDefault:
            if fs is None:
                # No parent, so default to steady-state and warn user
                _log.warning('{} is a top level flowsheet, but dynamic flag '
                             'set to useDefault. Dynamic '
                             'flag set to False by default'.format(self.name))
                self.config.dynamic = False

            else:
                # Get dynamic flag from parent flowsheet
                self.config.dynamic = fs.config.dynamic

        # Check for case when dynamic=True, but parent dynamic=False
        elif self.config.dynamic is True:
            if fs is not None and fs.config.dynamic is False:
                raise DynamicError(
                    '{} 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))

        # Validate units for time domain
        if self.config.time_units is None and self.config.dynamic:
            _log.warning("DEPRECATED: No units were specified for the time "
                         "domain. Users should provide units via the "
                         "time_units configuration argument.")
        elif self.config.time_units is None and not self.config.dynamic:
            _log.info_high("DEPRECATED: No units were specified for the time "
                           "domain. Users should provide units via the "
                           "time_units configuration argument.")
        elif not isinstance(self.config.time_units, _PyomoUnit):
            raise ConfigurationError(
                "{} unrecognised value for time_units argument. This must be "
                "a Pyomo Unit object (not a compound unit).".format(self.name))

        if self.config.time is not None:
            # Validate user provided time domain
            if (self.config.dynamic is True
                    and not isinstance(self.config.time, ContinuousSet)):
                raise DynamicError(
                    '{} was set as a dynamic flowsheet, but time domain '
                    'provided was not a ContinuousSet.'.format(self.name))
            self._time_units = self.config.time_units
        else:
            # If no parent flowsheet, set up time domain
            if fs is None:
                # Create time domain
                if self.config.dynamic:
                    # Check if time_set has at least two points
                    if len(self.config.time_set) < 2:
                        # Check if time_set is at default value
                        if self.config.time_set == [0.0]:
                            # If default, set default end point to be 1.0
                            self.config.time_set = [0.0, 1.0]
                        else:
                            # Invalid user input, raise Excpetion
                            raise DynamicError(
                                "Flowsheet provided with invalid "
                                "time_set attribute - must have at "
                                "least two values (start and end).")
                    # For dynamics, need a ContinuousSet
                    self.time = ContinuousSet(initialize=self.config.time_set)
                else:
                    # For steady-state, use an ordered Set
                    self.time = pe.Set(initialize=self.config.time_set,
                                       ordered=True)
                self._time_units = self.config.time_units

                # Set time config argument as reference to time domain
                self.config.time = self.time
            else:
                # Set time config argument to parent time
                self.config.time = fs.config.time
                self._time_units = fs._time_units
Exemple #13
0
class ReactionParameterBlock(ProcessBlockData,
                             property_meta.HasPropertyClassMetadata):
    """
        This is the base class for reaction parameter blocks. These are blocks
        that contain a set of parameters associated with a specific reaction
        package, and are linked to by all instances of that reaction package.
    """
    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "property_package",
        ConfigValue(
            description="Reference to associated PropertyPackageParameter "
            "object",
            domain=is_physical_parameter_block))
    CONFIG.declare(
        "default_arguments",
        ConfigBlock(
            description="Default arguments to use with Property Package",
            implicit=True))

    def build(self):
        """
        General build method for ReactionParameterBlocks. Inheriting models
        should call super().build.

        Args:
            None

        Returns:
            None
        """
        super(ReactionParameterBlock, self).build()

        # TODO: Need way to tie reaction package to a specfic property package
        self._validate_property_parameter_units()
        self._validate_property_parameter_properties()

    def _validate_property_parameter_units(self):
        """
        Checks that the property parameter block associated with the
        reaction block uses the same set of default units.
        """
        r_units = self.get_metadata().default_units
        prop_units = self.config.property_package.get_metadata().default_units
        for u in r_units:
            try:
                if prop_units[u] != r_units[u]:
                    raise KeyError()
            except KeyError:
                raise PropertyPackageError(
                    '{} the property package associated with this '
                    'reaction package does not use the same set of '
                    'units of measurement ({}). Please choose a '
                    'property package which uses the same units.'.format(
                        self.name, u))

    def _validate_property_parameter_properties(self):
        """
        Checks that the property parameter block associated with the
        reaction block supports the necessary properties with correct units.
        """
        req_props = self.get_metadata().required_properties
        supp_props = self.config.property_package.get_metadata().properties

        for p in req_props:
            if p not in supp_props:
                raise PropertyPackageError(
                    '{} the property package associated with this '
                    'reaction package does not support the necessary '
                    'property, {}. Please choose a property package '
                    'which supports all required properties.'.format(
                        self.name, p))
            elif supp_props[p]['method'] is False:
                raise PropertyPackageError(
                    '{} the property package associated with this '
                    'reaction package does not support the necessary '
                    'property, {}. Please choose a property package '
                    'which supports all required properties.'.format(
                        self.name, p))

            # Check property units
            if req_props[p]['units'] != supp_props[p]['units']:
                raise PropertyPackageError(
                    '{} the units associated with property {} in this '
                    'reaction package ({}) do not match with the units '
                    'used in the assoicated property package ({}). Please '
                    'choose a property package which used the same '
                    'units for all properties.'.format(self.name, p,
                                                       req_props[p]['units'],
                                                       supp_props[p]['units']))
Exemple #14
0
class ReactionParameterBlock(ProcessBlockData,
                             property_meta.HasPropertyClassMetadata):
    """
        This is the base class for reaction parameter blocks. These are blocks
        that contain a set of parameters associated with a specific reaction
        package, and are linked to by all instances of that reaction package.
    """
    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "property_package",
        ConfigValue(
            description="Reference to associated PropertyPackageParameter "
            "object",
            domain=is_physical_parameter_block))
    CONFIG.declare(
        "default_arguments",
        ConfigBlock(
            description="Default arguments to use with Property Package",
            implicit=True))

    def __init__(self, *args, **kwargs):
        self.__reaction_block_class = None
        super().__init__(*args, **kwargs)

    def build(self):
        """
        General build method for ReactionParameterBlocks. Inheriting models
        should call super().build.

        Args:
            None

        Returns:
            None
        """
        super(ReactionParameterBlock, self).build()

        if not hasattr(self, "_reaction_block_class"):
            self._reaction_block_class = None

        # TODO: Need way to tie reaction package to a specfic property package
        self._validate_property_parameter_units()
        self._validate_property_parameter_properties()

        # This is a dict to store default property scaling factors. They are
        # defined in the parameter block to provide a universal default for
        # quantities in a particular kind of state block.  For example, you can
        # set flow scaling once instead of for every state block. Some of these
        # may be left for the user to set and some may be defined in a property
        # module where reasonable defaults can be defined a priori. See
        # set_default_scaling, get_default_scaling, and unset_default_scaling
        self.default_scaling_factor = {}

    def set_default_scaling(self, attrbute, value, index=None):
        """Set a default scaling factor for a property.

        Args:
            attribute: property attribute name
            value: default scaling factor
            index: for indexed properties, if this is not provied the scaling
                factor default applies to all indexed elements where specific
                indexes are no specifcally specified.

        Returns:
            None
        """
        self.default_scaling_factor[(attrbute, index)] = value

    def unset_default_scaling(self, attrbute, index=None):
        """Remove a previously set default value

        Args:
            attribute: property attribute name
            index: optional index for indexed properties

        Returns:
            None
        """
        try:
            del self.default_scaling_factor[(attrbute, index)]
        except KeyError:
            pass

    def get_default_scaling(self, attrbute, index=None):
        """ Returns a default scale factor for a property

        Args:
            attribute: property attribute name
            index: optional index for indexed properties

        Returns:
            None
        """
        try:
            # If a specific component data index exists
            return self.default_scaling_factor[(attrbute, index)]
        except KeyError:
            try:
                # indexed, but no specifc index?
                return self.default_scaling_factor[(attrbute, None)]
            except KeyError:
                # Can't find a default scale factor for what you asked for
                return None

    @property
    def reaction_block_class(self):
        if self._reaction_block_class is not None:
            return self._reaction_block_class
        else:
            raise AttributeError(
                "{} has not assigned a ReactionBlock class to be associated "
                "with this reaction package. Please contact the developer of "
                "the reaction package.".format(self.name))

    @reaction_block_class.setter
    def reaction_block_class(self, val):
        _log.warning("DEPRECATED: reaction_block_class should not be set "
                     "directly. Property package developers should set the "
                     "_reaction_block_class attribute instead.")
        self._reaction_block_class = val

    def build_reaction_block(self, *args, **kwargs):
        """
        Methods to construct a ReactionBlock assoicated with this
        ReactionParameterBlock. This will automatically set the parameters
        construction argument for the ReactionBlock.

        Returns:
            ReactionBlock

        """
        default = kwargs.pop("default", {})
        initialize = kwargs.pop("initialize", {})

        if initialize == {}:
            default["parameters"] = self
        else:
            for i in initialize.keys():
                initialize[i]["parameters"] = self

        return self.reaction_block_class(*args,
                                         **kwargs,
                                         default=default,
                                         initialize=initialize)

    def _validate_property_parameter_units(self):
        """
        Checks that the property parameter block associated with the
        reaction block uses the same set of default units.
        """
        r_units = self.get_metadata().default_units
        prop_units = self.config.property_package.get_metadata().default_units
        for u in r_units:
            try:
                # TODO: This check is for backwards compatability with
                # pre-units property packages. It can be removed once these are
                # fully deprecated.
                if isinstance(prop_units[u],
                              str) and (prop_units[u] != r_units[u]):
                    raise KeyError()
                elif prop_units[u] is not r_units[u]:
                    raise KeyError()
            except KeyError:
                raise PropertyPackageError(
                    '{} the property package associated with this '
                    'reaction package does not use the same set of '
                    'units of measurement ({}). Please choose a '
                    'property package which uses the same units.'.format(
                        self.name, u))

    def _validate_property_parameter_properties(self):
        """
        Checks that the property parameter block associated with the
        reaction block supports the necessary properties with correct units.
        """
        req_props = self.get_metadata().required_properties
        supp_props = self.config.property_package.get_metadata().properties

        for p in req_props:
            if p not in supp_props:
                raise PropertyPackageError(
                    '{} the property package associated with this '
                    'reaction package does not support the necessary '
                    'property, {}. Please choose a property package '
                    'which supports all required properties.'.format(
                        self.name, p))
            elif supp_props[p]['method'] is False:
                raise PropertyPackageError(
                    '{} the property package associated with this '
                    'reaction package does not support the necessary '
                    'property, {}. Please choose a property package '
                    'which supports all required properties.'.format(
                        self.name, p))

            # Check property units
            if req_props[p]['units'] != supp_props[p]['units']:
                raise PropertyPackageError(
                    '{} the units associated with property {} in this '
                    'reaction package ({}) do not match with the units '
                    'used in the assoicated property package ({}). Please '
                    'choose a property package which used the same '
                    'units for all properties.'.format(self.name, p,
                                                       req_props[p]['units'],
                                                       supp_props[p]['units']))
class ControlVolumeBlockData(ProcessBlockData):
    """
    The ControlVolumeBlockData Class forms the base class for all IDAES
    ControlVolume models. The purpose of this class is to automate the tasks
    common to all control volume blockss and ensure that the necessary
    attributes of a control volume block are present.

    The most signfiicant role of the ControlVolumeBlockData class is to set up
    the construction arguments for the control volume block, automatically link
    to the time domain of the parent block, and to get the information about
    the property and reaction packages.
    """

    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([useDefault, True, False]),
                    default=useDefault,
                    description="Dynamic model flag",
                    doc="""Indicates whether this model will be dynamic,
**default** - useDefault.
**Valid values:** {
**useDefault** - get flag from parent,
**True** - set as a dynamic model,
**False** - set as a steady-state model}"""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=useDefault,
            domain=In([useDefault, True, False]),
            description="Holdup construction flag",
            doc="""Indicates whether holdup terms should be constructed or not.
Must be True if dynamic = True,
**default** - False.
**Valid values:** {
**True** - construct holdup terms,
**False** - do not construct holdup terms}"""))
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}"""))
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these, **default** - None. **Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "reaction_package",
        ConfigValue(
            default=None,
            domain=is_reaction_parameter_block,
            description="Reaction package to use for control volume",
            doc=
            """Reaction parameter object used to define reaction calculations,
**default** - None.
**Valid values:** {
**None** - no reaction package,
**ReactionParameterBlock** - a ReactionParameterBlock object.}"""))
    CONFIG.declare(
        "reaction_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing reaction packages",
            doc=
            """A ConfigBlock with arguments to be passed to a reaction block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see reaction package for documentation.}"""))
    CONFIG.declare(
        "auto_construct",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Argument indicating whether ControlVolume should "
            "automatically construct balance equations",
            doc="""If set to True, this argument will trigger the auto_construct
method which will attempt to construct a set of material, energy and momentum
balance equations based on the parent unit's config block. The parent unit must
have a config block which derives from CONFIG_Base,
**default** - False.
**Valid values:** {
**True** - use automatic construction,
**False** - do not use automatic construciton.}"""))

    def build(self):
        """
        General build method for Control Volumes blocks. This method calls a
        number of sub-methods which automate the construction of expected
        attributes of all ControlVolume blocks.

        Inheriting models should call `super().build`.

        Args:
            None

        Returns:
            None
        """
        super(ControlVolumeBlockData, self).build()

        # Setup dynamics flag and time domain
        self._setup_dynamics()

        # Get property package details
        self._get_property_package()

        # Get indexing sets
        self._get_indexing_sets()

        # Get reaction package details (as necessary)
        self._get_reaction_package()

        if self.config.auto_construct is True:
            self._auto_construct()

    def add_geometry(self, *args, **kwargs):
        """
        Method for defining the geometry of the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_geometry. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_material_balances(self,
                              balance_type=MaterialBalanceType.componentPhase,
                              **kwargs):
        """
        General method for adding material balances to a control volume.
        This method makes calls to specialised sub-methods for each type of
        material balance.

        Args:
            balance_type - MaterialBalanceType Enum indicating which type of
                    material balance should be constructed.
            has_rate_reactions - whether default generation terms for rate
                    reactions should be included in material balances
            has_equilibrium_reactions - whether generation terms should for
                    chemical equilibrium reactions should be included in
                    material balances
            has_phase_equilibrium - whether generation terms should for phase
                    equilibrium behaviour should be included in material
                    balances
            has_mass_transfer - whether generic mass transfer terms should be
                    included in material balances
            custom_molar_term - a Pyomo Expression representing custom terms to
                    be included in material balances on a molar basis.
            custom_mass_term - a Pyomo Expression representing custom terms to
                    be included in material balances on a mass basis.

        Returns:
            Constraint objects constructed by sub-method
        """
        if balance_type == MaterialBalanceType.none:
            mb = None
        elif balance_type == MaterialBalanceType.componentPhase:
            mb = self.add_phase_component_balances(**kwargs)
        elif balance_type == MaterialBalanceType.componentTotal:
            mb = self.add_total_component_balances(**kwargs)
        elif balance_type == MaterialBalanceType.elementTotal:
            mb = self.add_total_element_balances(**kwargs)
        elif balance_type == MaterialBalanceType.total:
            mb = self.add_total_material_balances(**kwargs)
        else:
            raise ConfigurationError(
                "{} invalid balance_type for add_material_balances."
                "Please contact the unit model developer with this bug.".
                format(self.name))

        return mb

    def add_energy_balances(self,
                            balance_type=EnergyBalanceType.enthalpyPhase,
                            **kwargs):
        """
        General method for adding energy balances to a control volume.
        This method makes calls to specialised sub-methods for each type of
        energy balance.

        Args:
            balance_type (EnergyBalanceType): Enum indicating which type of
                energy balance should be constructed.
            has_heat_of_reaction (bool): whether terms for heat of reaction
                should be included in energy balance
            has_heat_transfer (bool): whether generic heat transfer terms
                should be included in energy balances
            has_work_transfer (bool): whether generic mass transfer terms
                should be included in energy balances
            custom_term (Expression): a Pyomo Expression representing custom
                terms to be included in energy balances

        Returns:
            Constraint objects constructed by sub-method
        """
        if balance_type == EnergyBalanceType.none:
            eb = None
        elif balance_type == EnergyBalanceType.enthalpyTotal:
            eb = self.add_total_enthalpy_balances(**kwargs)
        elif balance_type == EnergyBalanceType.enthalpyPhase:
            eb = self.add_phase_enthalpy_balances(**kwargs)
        elif balance_type == EnergyBalanceType.energyTotal:
            eb = self.add_total_energy_balances(**kwargs)
        elif balance_type == EnergyBalanceType.energyPhase:
            eb = self.add_phase_energy_balances(**kwargs)
        else:
            raise ConfigurationError(
                "{} invalid balance_type for add_energy_balances."
                "Please contact the unit model developer with this bug.".
                format(self.name))

        return eb

    def add_momentum_balances(self,
                              balance_type=MomentumBalanceType.pressureTotal,
                              **kwargs):
        """
        General method for adding momentum balances to a control volume.
        This method makes calls to specialised sub-methods for each type of
        momentum balance.

        Args:
            balance_type (MomentumBalanceType): Enum indicating which type of
                momentum balance should be constructed.
            has_pressure_change (bool): whether default generation terms for
                pressure change should be included in momentum balances
            custom_term (Expression): a Pyomo Expression representing custom
                terms to be included in momentum balances

        Returns:
            Constraint objects constructed by sub-method
        """
        if balance_type == MomentumBalanceType.none:
            mb = None
        elif balance_type == MomentumBalanceType.pressureTotal:
            mb = self.add_total_pressure_balances(**kwargs)
        elif balance_type == MomentumBalanceType.pressurePhase:
            mb = self.add_phase_pressure_balances(**kwargs)
        elif balance_type == MomentumBalanceType.momentumTotal:
            mb = self.add_total_momentum_balances(**kwargs)
        elif balance_type == MomentumBalanceType.momentumPhase:
            mb = self.add_phase_momentum_balances(**kwargs)
        else:
            raise ConfigurationError(
                "{} invalid balance_type for add_momentum_balances."
                "Please contact the unit model developer with this bug.".
                format(self.name))

        return mb

    def _auto_construct(self):
        """
        Placeholder _auto_construct method to ensure a useful exception is
        returned if auto_build is set to True but something breaks in the
        process. Derived ControlVolume classes should overload this.

        Args:
            None

        Returns:
            None
        """
        parent = self.parent_block()

        self.add_geometry()
        self.add_state_blocks()
        self.add_reaction_blocks()

        self.add_material_balances(
            material_balance_type=parent.config.material_balance_type,
            has_rate_reactions=parent.config.has_rate_reactions,
            has_equilibrium_reactions=parent.config.has_equilibrium_reactions,
            has_phase_equilibrium=parent.config.has_phase_equilibrium,
            has_mass_transfer=parent.config.has_mass_transfer)

        self.add_energy_balances(
            energy_balance_type=parent.config.energy_balance_type,
            has_heat_of_reaction=parent.config.has_heat_of_reaction,
            has_heat_transfer=parent.config.has_heat_transfer,
            has_work_transfer=parent.config.has_work_transfer)

        self.add_momentum_balances(
            has_pressure_change=parent.config.has_pressure_change)

        try:
            self.apply_transformation()
        except AttributeError:
            pass

    # Add placeholder methods for adding property and reaction packages
    def add_state_blocks(self, *args, **kwargs):
        """
        Method for adding StateBlocks to the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_state_blocks. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_reaction_blocks(self, *args, **kwargs):
        """
        Method for adding ReactionBlocks to the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_reaction_blocks. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    # Add placeholder methods for all types of material, energy and momentum
    # balance equations which return NotImplementedErrors
    def add_phase_component_balances(self, *args, **kwargs):
        """
        Method for adding material balances indexed by phase and component to
        the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_phase_component_material_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_total_component_balances(self, *args, **kwargs):
        """
        Method for adding material balances indexed by component to
        the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_total_component_material_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_total_element_balances(self, *args, **kwargs):
        """
        Method for adding total elemental material balances indexed to
        the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_total_element_material_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_total_material_balances(self, *args, **kwargs):
        """
        Method for adding a total material balance to
        the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_total_material_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_phase_enthalpy_balances(self, *args, **kwargs):
        """
        Method for adding enthalpy balances indexed by phase to
        the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_phase_enthalpy_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_total_enthalpy_balances(self, *args, **kwargs):
        """
        Method for adding a total enthalpy balance to
        the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_total_enthalpy_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_phase_energy_balances(self, *args, **kwargs):
        """
        Method for adding energy balances (including kinetic energy) indexed by
        phase to the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_phase_energy_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_total_energy_balances(self, *args, **kwargs):
        """
        Method for adding a total energy balance (including kinetic energy)
        to the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_total_energy_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_phase_pressure_balances(self, *args, **kwargs):
        """
        Method for adding pressure balances indexed by
        phase to the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_phase_pressure_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_total_pressure_balances(self, *args, **kwargs):
        """
        Method for adding a total pressure balance to the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_total_pressure_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_phase_momentum_balances(self, *args, **kwargs):
        """
        Method for adding momentum balances indexed by phase to the control
        volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_phase_momentum_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def add_total_momentum_balances(self, *args, **kwargs):
        """
        Method for adding a total momentum balance to the control volume.

        See specific control volume documentation for details.
        """
        raise NotImplementedError(
            "{} control volume class has not implemented a method for "
            "add_total_momentum_balances. Please contact the "
            "developer of the ControlVolume class you are using.".format(
                self.name))

    def _rxn_rate_conv(b, t, x, j, has_rate_reactions):
        """
        Method to determine conversion term for reaction rate terms in material
        balance equations. This method gets the basis of the material flow
        and reaction rate terms and determines the correct conversion factor.
        """
        # If rate reactions are not required, skip the rest and return 1
        if not has_rate_reactions:
            return 1

        if x is None:
            # 0D control volume
            flow_basis = b.properties_out[t].get_material_flow_basis()
            prop = b.properties_out[t]
            rxn_basis = b.reactions[t].get_reaction_rate_basis()
        else:
            # 1D control volume
            flow_basis = b.properties[t, x].get_material_flow_basis()
            prop = b.properties[t, x]
            rxn_basis = b.reactions[t, x].get_reaction_rate_basis()

        # Check for undefined basis
        if flow_basis == MaterialFlowBasis.other:
            raise ConfigurationError(
                "{} contains reaction terms, but the property package "
                "used an undefined basis (MaterialFlowBasis.other). "
                "Rate based reaction terms require the property "
                "package to define the basis of the material flow "
                "terms.".format(b.name))
        if rxn_basis == MaterialFlowBasis.other:
            raise ConfigurationError(
                "{} contains reaction terms, but the reaction package "
                "used an undefined basis (MaterialFlowBasis.other). "
                "Rate based reaction terms require the reaction "
                "package to define the basis of the reaction rate "
                "terms.".format(b.name))

        try:
            if flow_basis == rxn_basis:
                return 1
            elif (flow_basis == MaterialFlowBasis.mass
                  and rxn_basis == MaterialFlowBasis.molar):
                return prop.mw[j]
            elif (flow_basis == MaterialFlowBasis.molar
                  and rxn_basis == MaterialFlowBasis.mass):
                return 1 / prop.mw[j]
            else:
                raise BurntToast(
                    "{} encountered unrecognsied combination of bases "
                    "for reaction rate terms. Please contact the IDAES"
                    " developers with this bug.".format(b.name))
        except AttributeError:
            raise PropertyNotSupportedError(
                "{} property package does not support "
                "molecular weight (mw), which is required for "
                "using property and reaction packages with "
                "different bases.".format(b.name))
class MomentumBalanceType(Enum):
    none = 0
    pressureTotal = 1
    pressurePhase = 2
    momentumTotal = 3
    momentumPhase = 4


# Enumerate options for flow direction
class FlowDirection(Enum):
    forward = 1
    backward = 2


# Set up example ConfigBlock that will work with ControlVolume autobuild method
CONFIG_Template = ProcessBlockData.CONFIG()
CONFIG_Template.declare(
    "dynamic",
    ConfigValue(default=useDefault,
                domain=In([useDefault, True, False]),
                description="Dynamic model flag",
                doc="""Indicates whether this model will be dynamic,
**default** - useDefault.
**Valid values:** {
**useDefault** - get flag from parent,
**True** - set as a dynamic model,
**False** - set as a steady-state model}"""))
CONFIG_Template.declare(
    "has_holdup",
    ConfigValue(
        default=False,
class ReactionParameterBlock(ProcessBlockData,
                             property_meta.HasPropertyClassMetadata):
    """
        This is the base class for reaction parameter blocks. These are blocks
        that contain a set of parameters associated with a specific reaction
        package, and are linked to by all instances of that reaction package.
    """
    # Create Class ConfigBlock
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "property_package",
        ConfigValue(
            description="Reference to associated PropertyPackageParameter "
            "object",
            domain=is_physical_parameter_block))
    CONFIG.declare(
        "default_arguments",
        ConfigBlock(
            description="Default arguments to use with Property Package",
            implicit=True))

    def __init__(self, *args, **kwargs):
        self.__reaction_block_class = None
        super().__init__(*args, **kwargs)

    def build(self):
        """
        General build method for ReactionParameterBlocks. Inheriting models
        should call super().build.

        Args:
            None

        Returns:
            None
        """
        super(ReactionParameterBlock, self).build()

        if not hasattr(self, "_reaction_block_class"):
            self._reaction_block_class = None

        # TODO: Need way to tie reaction package to a specfic property package
        self._validate_property_parameter_units()
        self._validate_property_parameter_properties()

    @property
    def reaction_block_class(self):
        if self._reaction_block_class is not None:
            return self._reaction_block_class
        else:
            raise AttributeError(
                "{} has not assigned a ReactionBlock class to be associated "
                "with this reaction package. Please contact the developer of "
                "the reaction package.".format(self.name))

    @reaction_block_class.setter
    def reaction_block_class(self, val):
        _log.warning("DEPRECATED: reaction_block_class should not be set "
                     "directly. Property package developers should set the "
                     "_reaction_block_class attribute instead.")
        self._reaction_block_class = val

    def build_reaction_block(self, *args, **kwargs):
        """
        Methods to construct a ReactionBlock assoicated with this
        ReactionParameterBlock. This will automatically set the parameters
        construction argument for the ReactionBlock.

        Returns:
            ReactionBlock

        """
        default = kwargs.pop("default", {})
        initialize = kwargs.pop("initialize", {})

        if initialize == {}:
            default["parameters"] = self
        else:
            for i in initialize.keys():
                initialize[i]["parameters"] = self

        return self.reaction_block_class(*args,
                                         **kwargs,
                                         default=default,
                                         initialize=initialize)

    def _validate_property_parameter_units(self):
        """
        Checks that the property parameter block associated with the
        reaction block uses the same set of default units.
        """
        r_units = self.get_metadata().default_units
        prop_units = self.config.property_package.get_metadata().default_units
        for u in r_units:
            try:
                # TODO: This check is for backwards compatability with
                # pre-units property packages. It can be removed once these are
                # fully deprecated.
                if isinstance(prop_units[u],
                              str) and (prop_units[u] != r_units[u]):
                    raise KeyError()
                elif prop_units[u] is not r_units[u]:
                    raise KeyError()
            except KeyError:
                raise PropertyPackageError(
                    '{} the property package associated with this '
                    'reaction package does not use the same set of '
                    'units of measurement ({}). Please choose a '
                    'property package which uses the same units.'.format(
                        self.name, u))

    def _validate_property_parameter_properties(self):
        """
        Checks that the property parameter block associated with the
        reaction block supports the necessary properties with correct units.
        """
        req_props = self.get_metadata().required_properties
        supp_props = self.config.property_package.get_metadata().properties

        for p in req_props:
            if p not in supp_props:
                raise PropertyPackageError(
                    '{} the property package associated with this '
                    'reaction package does not support the necessary '
                    'property, {}. Please choose a property package '
                    'which supports all required properties.'.format(
                        self.name, p))
            elif supp_props[p]['method'] is False:
                raise PropertyPackageError(
                    '{} the property package associated with this '
                    'reaction package does not support the necessary '
                    'property, {}. Please choose a property package '
                    'which supports all required properties.'.format(
                        self.name, p))

            # Check property units
            if req_props[p]['units'] != supp_props[p]['units']:
                raise PropertyPackageError(
                    '{} the units associated with property {} in this '
                    'reaction package ({}) do not match with the units '
                    'used in the assoicated property package ({}). Please '
                    'choose a property package which used the same '
                    'units for all properties.'.format(self.name, p,
                                                       req_props[p]['units'],
                                                       supp_props[p]['units']))