Esempio n. 1
0
def _define_feedwater_heater_0D_config(config):
    config.declare("has_drain_mixer", ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Add a mixer to the inlet of the condensing section",
            doc="""Add a mixer to the inlet of the condensing section to add
water from the drain of another feedwaterheater to the steam, if True"""))
    config.declare("has_desuperheat", ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Add a mixer desuperheat section to the heat exchanger",
            doc="Add a mixer desuperheat section to the heat exchanger"))
    config.declare("has_drain_cooling", ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Add a section after condensing section cool condensate.",
            doc="Add a section after condensing section to cool condensate."))
    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("condense", HeatExchangerData.CONFIG())
    config.declare("desuperheat", HeatExchangerData.CONFIG())
    config.declare("cooling", HeatExchangerData.CONFIG())
Esempio n. 2
0
class ReactionBlockData(ReactionBlockDataBase):
    CONFIG = ConfigBlock(implicit=True)

    def build(self):
        super(ReactionBlockData, self).build()

        self.reaction_rate = Var(["r1", "r2"],
                                 units=units.mol/units.m**3/units.s)

        self.dh_rxn = {"r1": 10*units.J/units.mol,
                       "r2": 20*units.J/units.mol,
                       "e1": 30*units.J/units.mol,
                       "e2": 40*units.J/units.mol}

    def model_check(self):
        self.check = True

    def get_reaction_rate_basis(b):
        if b.config.parameters.basis_switch == 1:
            return MaterialFlowBasis.molar
        elif b.config.parameters.basis_switch == 2:
            return MaterialFlowBasis.mass
        else:
            return MaterialFlowBasis.other
Esempio n. 3
0
class NoPressureStateBlockData(StateBlockData):
    CONFIG = ConfigBlock(implicit=True)

    def build(self):
        super(NoPressureStateBlockData, self).build()

        self.flow_vol = Var(initialize=20)
        self.flow_mol_phase_comp = Var(self._params.phase_list,
                                       self._params.component_list,
                                       initialize=2)
        self.temperature = Var(initialize=300)
        self.test_var = Var(initialize=1)

    def get_material_flow_terms(b, p, j):
        return b.test_var

    def get_enthalpy_flow_terms(b, p):
        return b.test_var

    def default_material_balance_type(self):
        return MaterialBalanceType.componentPhase

    def default_energy_balance_type(self):
        return EnergyBalanceType.enthalpyTotal
Esempio n. 4
0
class StateTestBlockData(StateBlockData):
    CONFIG = ConfigBlock(implicit=True)

    def build(self):
        super(StateTestBlockData, self).build()

        self.phase_list = Set(initialize=["p1", "p2"])
        self.component_list = Set(initialize=["c1", "c2"])
        self.phase_equilibrium_idx = Set(initialize=["e1", "e2"])
        self.phase_equilibrium_list = \
            {"e1": ["c1", ("p1", "p2")],
             "e2": ["c2", ("p1", "p2")]}

        self.pressure = Var(initialize=1e5)
        self.flow_mol_phase_comp = Var(self.phase_list,
                                       self.component_list,
                                       initialize=1)
        self.enth_mol_phase = Var(self.phase_list, initialize=2)
        self.enth_mol = Var(initialize=2)  # total molar enthalpy (both phases)
        self.temperature = Var(initialize=5)

    def get_material_flow_terms(b, p, j):
        return b.flow_mol_phase_comp[p, j]

    def get_enthalpy_flow_terms(b, p):
        return b.enth_mol_phase[p]

    def define_state_vars(self):
        return {
            "component_flow": self.flow_mol_phase_comp,
            "enthalpy": self.enth_mol_phase,
            "pressure": self.pressure
        }

    def model_check(self):
        self.check = True
Esempio n. 5
0
def _add_subsolver_configs(CONFIG):
    """Adds the subsolver-related configurations.

    Parameters
    ----------
    CONFIG : ConfigBlock
        The specific configurations for MindtPy.
    """
    CONFIG.declare(
        'nlp_solver',
        ConfigValue(
            default='ipopt',
            domain=In(['ipopt', 'gams', 'baron']),
            description='NLP subsolver name',
            doc=
            'Which NLP subsolver is going to be used for solving the nonlinear'
            'subproblems.'))
    CONFIG.declare(
        'nlp_solver_args',
        ConfigBlock(
            implicit=True,
            description='NLP subsolver options',
            doc='Which NLP subsolver options to be passed to the solver while '
            'solving the nonlinear subproblems.'))
    CONFIG.declare(
        'mip_solver',
        ConfigValue(
            default='glpk',
            domain=In([
                'gurobi', 'cplex', 'cbc', 'glpk', 'gams', 'gurobi_persistent',
                'cplex_persistent'
            ]),
            description='MIP subsolver name',
            doc='Which MIP subsolver is going to be used for solving the mixed-'
            'integer main problems.'))
    CONFIG.declare(
        'mip_solver_args',
        ConfigBlock(
            implicit=True,
            description='MIP subsolver options',
            doc='Which MIP subsolver options to be passed to the solver while '
            'solving the mixed-integer main problems.'))
    CONFIG.declare(
        'mip_solver_mipgap',
        ConfigValue(default=1E-4,
                    domain=PositiveFloat,
                    description='Mipgap passed to MIP solver.'))
    CONFIG.declare(
        'threads',
        ConfigValue(default=0,
                    domain=NonNegativeInt,
                    description='Threads',
                    doc='Threads used by MIP solver and NLP solver.'))
    CONFIG.declare(
        'regularization_mip_threads',
        ConfigValue(
            default=0,
            domain=NonNegativeInt,
            description='regularization MIP threads',
            doc=
            'Threads used by MIP solver to solve regularization main problem.')
    )
    CONFIG.declare(
        'solver_tee',
        ConfigValue(
            default=False,
            description=
            'Stream the output of MIP solver and NLP solver to terminal.',
            domain=bool))
    CONFIG.declare(
        'mip_solver_tee',
        ConfigValue(default=False,
                    description='Stream the output of MIP solver to terminal.',
                    domain=bool))
    CONFIG.declare(
        'nlp_solver_tee',
        ConfigValue(default=False,
                    description='Stream the output of nlp solver to terminal.',
                    domain=bool))
    CONFIG.declare(
        'mip_regularization_solver',
        ConfigValue(
            default=None,
            domain=In([
                'gurobi', 'cplex', 'cbc', 'glpk', 'gams', 'gurobi_persistent',
                'cplex_persistent'
            ]),
            description='MIP subsolver for regularization problem',
            doc=
            'Which MIP subsolver is going to be used for solving the regularization problem.'
        ))
Esempio n. 6
0

class GenericReactionPackageError(PropertyPackageError):
    # Error message for when a property is called for but no option provided
    def __init__(self, block, prop):
        self.prop = prop
        self.block = block

    def __str__(self):
        return f"Generic Reaction Package instance {self.block} called for " \
               f"{self.prop}, but was not provided with a method " \
               f"for this property. Please add a method for this property " \
               f"in the reaction parameter configuration."


rxn_config = ConfigBlock()
rxn_config.declare(
    "stoichiometry",
    ConfigValue(domain=dict,
                description="Stoichiometry of reaction",
                doc="Dict describing stoichiometry of reaction"))
rxn_config.declare(
    "heat_of_reaction",
    ConfigValue(
        description="Method for calculating specific heat of reaction",
        doc="Valid Python class containing instructions on how to calculate "
        "the heat of reaction for this reaction."))
rxn_config.declare(
    "concentration_form",
    ConfigValue(
        default=None,
Esempio n. 7
0
class ZeroOrderBaseData(UnitModelBlockData):
    """
    Standard base class for zero order unit models.

    This class contains the basic consistency checks and common methods for
    zero order type models.
    """

    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc="""All zero-order models are steady-state only""",
        ),
    )
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Zero order models do not include holdup""",
        ),
    )
    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,
        **PhysicalParameterObject** - a PhysicalParameterBlock 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(
        "database",
        ConfigValue(
            description=
            "An instance of a WaterTAP Database to use for parameters."),
    )
    CONFIG.declare(
        "process_subtype",
        ConfigValue(
            description=
            "Process subtype to use when looking up parameters from database."
        ),
    )

    def build(self):
        super().build()

        # Set a placeholder attributes
        # Placeholder for technology type string
        self._tech_type = None

        # Attribute indicating what parameters need to be fixed in the model
        self._has_recovery_removal = False
        self._fixed_perf_vars = []

        # Place holders for assigning methods
        self._initialize = None  # used to link to initization routine
        self._scaling = None  # used to link to scaling routine
        self._get_Q = None  # used to provide inlet volumetric flow

        # Attributed for storing contents of reporting output
        self._stream_table_dict = {}
        self._perf_var_dict = {}

        # Check that property package meets requirements
        if self.config.property_package.phase_list != ["Liq"]:
            raise ConfigurationError(
                f"{self.name} configured with invalid property package. "
                f"Zero-order models only support property packages with a "
                f"single phase named 'Liq'.")
        if not hasattr(
                self.config.property_package, "solvent_set"
        ) or self.config.property_package.solvent_set != ["H2O"]:
            raise ConfigurationError(
                f"{self.name} configured with invalid property package. "
                f"Zero-order models only support property packages which "
                f"include 'H2O' as the only Solvent.")
        if not hasattr(self.config.property_package, "solute_set"):
            raise ConfigurationError(
                f"{self.name} configured with invalid property package. "
                f"Zero-order models require property packages to declare all "
                f"dissolved species as Solutes.")
        if (len(self.config.property_package.solute_set) !=
                len(self.config.property_package.component_list) - 1):
            raise ConfigurationError(
                f"{self.name} configured with invalid property package. "
                f"Zero-order models only support `H2O` as a solvent and all "
                f"other species as Solutes.")

    def initialize_build(self,
                         state_args=None,
                         outlvl=idaeslog.NOTSET,
                         solver=None,
                         optarg=None):
        """
        Placeholder initialization routine, raises NotImplementedError
        """
        if self._initialize is None or not callable(self._initialize):
            raise NotImplementedError()
        else:
            self._initialize(self,
                             state_args=None,
                             outlvl=idaeslog.NOTSET,
                             solver=None,
                             optarg=None)

    def calculate_scaling_factors(self):
        """
        Placeholder scaling routine, should be overloaded by derived classes
        """
        super().calculate_scaling_factors()

        if callable(self._scaling):
            self._scaling(self)

    def load_parameters_from_database(self, use_default_removal=False):
        """
        Method to load parameters for from database.

        Args:
            use_default_removal - (optional) indicate whether to use defined
                                  default removal fraction if no specific value
                                  defined in database

        Returns:
            None
        """
        # Get parameter dict from database
        if self._tech_type is None:
            raise NotImplementedError(
                f"{self.name} derived zero order unit model has not "
                f"implemented the _tech_type attribute. This is required "
                f"to identify the database file to load parameters from.")

        # Get parameter dict from database
        pdict = self.config.database.get_unit_operation_parameters(
            self._tech_type, subtype=self.config.process_subtype)

        if self._has_recovery_removal:
            self.set_recovery_and_removal(pdict, use_default_removal)

        for v in self._fixed_perf_vars:
            self.set_param_from_data(v, pdict)

    def set_recovery_and_removal(self, data, use_default_removal=False):
        """
        Common utility method for setting values of recovery and removal
        fractions.

        Args:
            data - dict of parameter values to use when fixing variables
            use_default_removal - (optional) indicate whether to use defined
                                  default removal fraction if no specific value
                                  defined in database

        Returns:
            None
        """
        try:
            self.set_param_from_data(self.recovery_frac_mass_H2O, data)
        except KeyError:
            if self.recovery_frac_mass_H2O[:].fixed:
                pass
            else:
                raise

        for t, j in self.removal_frac_mass_solute:
            self.set_param_from_data(
                self.removal_frac_mass_solute[t, j],
                data,
                index=j,
                use_default_removal=use_default_removal,
            )

    def set_param_from_data(self,
                            parameter,
                            data,
                            index=None,
                            use_default_removal=False):
        """
        General method for setting parameter values from a dict of data
        returned from a database.

        Args:
            parameter - a Pyomo Var to be fixed to value from database
            data - dict of parameter values from database
            index - (optional) index to fix if parameter is an IndexedVar
            use_default_removal - (optional) indicate whether to use defined
                                  default removal fraction if no specific value
                                  defined in database

        Returns:
            None

        Raises:
            KeyError if values cannot be found for parameter in data dict

        """

        pname = parameter.parent_component().local_name

        try:
            pdata = data[pname]
        except KeyError:
            raise KeyError(
                f"{self.name} - database provided does not contain an entry "
                f"for {pname} for technology.")

        if index is not None:
            try:
                pdata = pdata[index]
            except KeyError:
                if pname == "removal_frac_mass_solute" and use_default_removal:
                    try:
                        pdata = data["default_removal_frac_mass_solute"]
                        index = "default"
                    except KeyError:
                        raise KeyError(
                            f"{self.name} - database provided does not "
                            f"contain an entry for {pname} with index {index} "
                            f"for technology and no default removal was "
                            f"specified.")
                else:
                    raise KeyError(
                        f"{self.name} - database provided does not contain "
                        f"an entry for {pname} with index {index} for "
                        f"technology.")

        try:
            val = pdata["value"]
        except KeyError:
            raise KeyError(
                f"{self.name} - no value provided for {pname} (index: "
                f"{index}) in database.")
        try:
            units = getattr(pyunits, pdata["units"])
        except KeyError:
            raise KeyError(
                f"{self.name} - no units provided for {pname} (index: "
                f"{index}) in database.")

        parameter.fix(val * units)
        _log.info_high(f"{parameter.name} fixed to value {val} {str(units)}")

    def get_inlet_flow(self, t):
        return self._get_Q(self, t)

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(self._stream_table_dict,
                                             time_point=time_point)

    def _get_performance_contents(self, time_point=0):
        var_dict = {}

        for k, v in self._perf_var_dict.items():
            if k in ["Solute Removal", "Reaction Extent", "Rejection"]:
                for j, vd in v[time_point, :].wildcard_items():
                    var_dict[f"{k} [{j}]"] = vd
            elif v.is_indexed():
                var_dict[k] = v[time_point]
            else:
                var_dict[k] = v

        return {"vars": var_dict}
Esempio n. 8
0
class MixerData(UnitModelBlockData):
    """
    This is a general purpose model for a Mixer block with the IDAES modeling
    framework. This block can be used either as a stand-alone Mixer unit
    operation, or as a sub-model within another unit operation.

    This model creates a number of StateBlocks to represent the incoming
    streams, then writes a set of phase-component material balances, an
    overall enthalpy balance and a momentum balance (2 options) linked to a
    mixed-state StateBlock. The mixed-state StateBlock can either be specified
    by the user (allowing use as a sub-model), or created by the Mixer.

    When being used as a sub-model, Mixer should only be used when a set
    of new StateBlocks are required for the streams to be mixed. It should not
    be used to mix streams from mutiple ControlVolumes in a single unit model -
    in these cases the unit model developer should write their own mixing
    equations.
    """

    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc="""Indicates whether this model will be dynamic or not,
**default** = False. Mixer blocks are always steady-state.""",
        ),
    )
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Mixer blocks do not contain holdup, thus this must be
False.""",
        ),
    )
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for mixer",
            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(
        "inlet_list",
        ConfigValue(
            domain=ListOf(str),
            description="List of inlet names",
            doc="""A list containing names of inlets,
**default** - None.
**Valid values:** {
**None** - use num_inlets argument,
**list** - a list of names to use for inlets.}""",
        ),
    )
    CONFIG.declare(
        "num_inlets",
        ConfigValue(
            domain=int,
            description="Number of inlets to unit",
            doc="""Argument indicating number (int) of inlets to construct, not
used if inlet_list arg is provided,
**default** - None.
**Valid values:** {
**None** - use inlet_list arg instead, or default to 2 if neither argument
provided,
**int** - number of inlets to create (will be named with sequential integers
from 1 to num_inlets).}""",
        ),
    )
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}""",
        ),
    )
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Calculate phase equilibrium in mixed stream",
            doc="""Argument indicating whether phase equilibrium should be
calculated for the resulting mixed stream,
**default** - False.
**Valid values:** {
**True** - calculate phase equilibrium in mixed stream,
**False** - do not calculate equilibrium in mixed stream.}""",
        ),
    )
    CONFIG.declare(
        "energy_mixing_type",
        ConfigValue(
            default=MixingType.extensive,
            domain=MixingType,
            description="Method to use when mixing energy flows",
            doc="""Argument indicating what method to use when mixing energy
flows of incoming streams,
**default** - MixingType.extensive.
**Valid values:** {
**MixingType.none** - do not include energy mixing equations,
**MixingType.extensive** - mix total enthalpy flows of each phase.}""",
        ),
    )
    CONFIG.declare(
        "momentum_mixing_type",
        ConfigValue(
            default=MomentumMixingType.minimize,
            domain=MomentumMixingType,
            description="Method to use when mixing momentum/pressure",
            doc="""Argument indicating what method to use when mixing momentum/
pressure of incoming streams,
**default** - MomentumMixingType.minimize.
**Valid values:** {
**MomentumMixingType.none** - do not include momentum mixing equations,
**MomentumMixingType.minimize** - mixed stream has pressure equal to the
minimimum pressure of the incoming streams (uses smoothMin operator),
**MomentumMixingType.equality** - enforces equality of pressure in mixed and
all incoming streams.,
**MomentumMixingType.minimize_and_equality** - add constraints for pressure
equal to the minimum pressure of the inlets and constraints for equality of
pressure in mixed and all incoming streams. When the model is initially built,
the equality constraints are deactivated.  This option is useful for switching
between flow and pressure driven simulations.}""",
        ),
    )
    CONFIG.declare(
        "mixed_state_block",
        ConfigValue(
            default=None,
            domain=is_state_block,
            description="Existing StateBlock to use as mixed stream",
            doc="""An existing state block to use as the outlet stream from the
Mixer block,
**default** - None.
**Valid values:** {
**None** - create a new StateBlock for the mixed stream,
**StateBlock** - a StateBock to use as the destination for the mixed stream.}
""",
        ),
    )
    CONFIG.declare(
        "construct_ports",
        ConfigValue(
            default=True,
            domain=Bool,
            description="Construct inlet and outlet Port objects",
            doc="""Argument indicating whether model should construct Port
objects linked to all inlet states and the mixed state,
**default** - True.
**Valid values:** {
**True** - construct Ports for all states,
**False** - do not construct Ports.""",
        ),
    )

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

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

        Args:
            None

        Returns:
            None
        """
        # Call super.build()
        super(MixerData, self).build()

        # Call setup methods from ControlVolumeBlockData
        self._get_property_package()
        self._get_indexing_sets()

        # Create list of inlet names
        inlet_list = self.create_inlet_list()

        # Build StateBlocks
        inlet_blocks = self.add_inlet_state_blocks(inlet_list)

        if self.config.mixed_state_block is None:
            mixed_block = self.add_mixed_state_block()
        else:
            mixed_block = self.get_mixed_state_block()

        mb_type = self.config.material_balance_type
        if mb_type == MaterialBalanceType.useDefault:
            t_ref = self.flowsheet().time.first()
            mb_type = mixed_block[t_ref].default_material_balance_type()

        if mb_type != MaterialBalanceType.none:
            self.add_material_mixing_equations(inlet_blocks=inlet_blocks,
                                               mixed_block=mixed_block,
                                               mb_type=mb_type)
        else:
            raise BurntToast("{} received unrecognised value for "
                             "material_mixing_type argument. This "
                             "should not occur, so please contact "
                             "the IDAES developers with this bug.".format(
                                 self.name))

        if self.config.energy_mixing_type == MixingType.extensive:
            self.add_energy_mixing_equations(inlet_blocks=inlet_blocks,
                                             mixed_block=mixed_block)
        elif self.config.energy_mixing_type == MixingType.none:
            pass
        else:
            raise ConfigurationError(
                "{} received unrecognised value for "
                "material_mixing_type argument. This "
                "should not occur, so please contact "
                "the IDAES developers with this bug.".format(self.name))

        # Add to try/expect to catch cases where pressure is not supported
        # by properties.
        try:
            if self.config.momentum_mixing_type == MomentumMixingType.minimize:
                self.add_pressure_minimization_equations(
                    inlet_blocks=inlet_blocks, mixed_block=mixed_block)
            elif (self.config.momentum_mixing_type ==
                  MomentumMixingType.equality):
                self.add_pressure_equality_equations(inlet_blocks=inlet_blocks,
                                                     mixed_block=mixed_block)
            elif (self.config.momentum_mixing_type ==
                  MomentumMixingType.minimize_and_equality):
                self.add_pressure_minimization_equations(
                    inlet_blocks=inlet_blocks, mixed_block=mixed_block)
                self.add_pressure_equality_equations(inlet_blocks=inlet_blocks,
                                                     mixed_block=mixed_block)
                self.pressure_equality_constraints.deactivate()
            elif self.config.momentum_mixing_type == MomentumMixingType.none:
                pass
            else:
                raise ConfigurationError(
                    "{} recieved unrecognised value for "
                    "momentum_mixing_type argument. This "
                    "should not occur, so please contact "
                    "the IDAES developers with this bug.".format(self.name))
        except PropertyNotSupportedError:
            raise PropertyNotSupportedError(
                "{} The property package supplied for this unit does not "
                "appear to support pressure, which is required for momentum "
                "mixing. Please set momentum_mixing_type to "
                "MomentumMixingType.none or provide a property package which "
                "supports pressure.".format(self.name))

        self.add_port_objects(inlet_list, inlet_blocks, mixed_block)

    def create_inlet_list(self):
        """
        Create list of inlet stream names based on config arguments.

        Returns:
            list of strings
        """
        if (self.config.inlet_list is not None
                and self.config.num_inlets is not None):
            # If both arguments provided and not consistent, raise Exception
            if len(self.config.inlet_list) != self.config.num_inlets:
                raise ConfigurationError(
                    "{} Mixer provided with both inlet_list and "
                    "num_inlets arguments, which were not consistent ("
                    "length of inlet_list was not equal to num_inlets). "
                    "PLease check your arguments for consistency, and "
                    "note that it is only necessary to provide one of "
                    "these arguments.".format(self.name))
        elif self.config.inlet_list is None and self.config.num_inlets is None:
            # If no arguments provided for inlets, default to num_inlets = 2
            self.config.num_inlets = 2

        # Create a list of names for inlet StateBlocks
        if self.config.inlet_list is not None:
            inlet_list = self.config.inlet_list
        else:
            inlet_list = [
                "inlet_" + str(n) for n in range(1, self.config.num_inlets + 1)
            ]

        return inlet_list

    def add_inlet_state_blocks(self, inlet_list):
        """
        Construct StateBlocks for all inlet streams.

        Args:
            list of strings to use as StateBlock names

        Returns:
            list of StateBlocks
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["defined_state"] = True

        # Create empty list to hold StateBlocks for return
        inlet_blocks = []

        # Create an instance of StateBlock for all inlets
        for i in inlet_list:
            i_obj = self.config.property_package.build_state_block(
                self.flowsheet().time,
                doc="Material properties at inlet",
                default=tmp_dict,
            )

            setattr(self, i + "_state", i_obj)

            inlet_blocks.append(getattr(self, i + "_state"))

        return inlet_blocks

    def add_mixed_state_block(self):
        """
        Constructs StateBlock to represent mixed stream.

        Returns:
            New StateBlock object
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium
        tmp_dict["defined_state"] = False

        self.mixed_state = self.config.property_package.build_state_block(
            self.flowsheet().time,
            doc="Material properties of mixed stream",
            default=tmp_dict,
        )

        return self.mixed_state

    def get_mixed_state_block(self):
        """
        Validates StateBlock provided in user arguments for mixed stream.

        Returns:
            The user-provided StateBlock or an Exception
        """
        # Sanity check to make sure method is not called when arg missing
        if self.config.mixed_state_block is None:
            raise BurntToast("{} get_mixed_state_block method called when "
                             "mixed_state_block argument is None. This should "
                             "not happen.".format(self.name))

        # Check that the user-provided StateBlock uses the same prop pack
        if (self.config.mixed_state_block[self.flowsheet().time.first()].
                config.parameters != self.config.property_package):
            raise ConfigurationError(
                "{} StateBlock provided in mixed_state_block argument "
                "does not come from the same property package as "
                "provided in the property_package argument. All "
                "StateBlocks within a Mixer must use the same "
                "property package.".format(self.name))

        return self.config.mixed_state_block

    def add_material_mixing_equations(self, inlet_blocks, mixed_block,
                                      mb_type):
        """
        Add material mixing equations.
        """
        pp = self.config.property_package
        # Get phase component list(s)
        pc_set = mixed_block.phase_component_set

        # Get units metadata
        units = pp.get_metadata()

        flow_basis = mixed_block[
            self.flowsheet().time.first()].get_material_flow_basis()
        if flow_basis == MaterialFlowBasis.molar:
            flow_units = units.get_derived_units("flow_mole")
        elif flow_basis == MaterialFlowBasis.mass:
            flow_units = units.get_derived_units("flow_mass")
        else:
            # Let this pass for now with no units
            flow_units = None

        if mb_type == MaterialBalanceType.componentPhase:
            # Create equilibrium generation term and constraints if required
            if self.config.has_phase_equilibrium is True:
                try:
                    self.phase_equilibrium_generation = Var(
                        self.flowsheet().time,
                        pp.phase_equilibrium_idx,
                        domain=Reals,
                        doc="Amount of generation in unit by phase equilibria",
                        units=flow_units)
                except AttributeError:
                    raise PropertyNotSupportedError(
                        "{} Property package does not contain a list of phase "
                        "equilibrium reactions (phase_equilibrium_idx), "
                        "thus does not support phase equilibrium.".format(
                            self.name))

            # Define terms to use in mixing equation
            def phase_equilibrium_term(b, t, p, j):
                if self.config.has_phase_equilibrium:
                    sd = {}
                    for r in pp.phase_equilibrium_idx:
                        if pp.phase_equilibrium_list[r][0] == j:
                            if (pp.phase_equilibrium_list[r][1][0] == p):
                                sd[r] = 1
                            elif (pp.phase_equilibrium_list[r][1][1] == p):
                                sd[r] = -1
                            else:
                                sd[r] = 0
                        else:
                            sd[r] = 0

                    return sum(b.phase_equilibrium_generation[t, r] * sd[r]
                               for r in pp.phase_equilibrium_idx)
                else:
                    return 0

            # Write phase-component balances
            @self.Constraint(
                self.flowsheet().time,
                pc_set,
                doc="Material mixing equations",
            )
            def material_mixing_equations(b, t, p, j):
                return 0 == (
                    sum(inlet_blocks[i][t].get_material_flow_terms(p, j)
                        for i in range(len(inlet_blocks))) -
                    mixed_block[t].get_material_flow_terms(p, j) +
                    phase_equilibrium_term(b, t, p, j))

        elif mb_type == MaterialBalanceType.componentTotal:
            # Write phase-component balances
            @self.Constraint(
                self.flowsheet().time,
                mixed_block.component_list,
                doc="Material mixing equations",
            )
            def material_mixing_equations(b, t, j):
                return 0 == sum(
                    sum(inlet_blocks[i][t].get_material_flow_terms(p, j)
                        for i in range(len(inlet_blocks))) -
                    mixed_block[t].get_material_flow_terms(p, j)
                    for p in mixed_block.phase_list if (p, j) in pc_set)

        elif mb_type == MaterialBalanceType.total:
            # Write phase-component balances
            @self.Constraint(self.flowsheet().time,
                             doc="Material mixing equations")
            def material_mixing_equations(b, t):
                return 0 == sum(
                    sum(
                        sum(inlet_blocks[i][t].get_material_flow_terms(p, j)
                            for i in range(len(inlet_blocks))) -
                        mixed_block[t].get_material_flow_terms(p, j)
                        for j in mixed_block.component_list
                        if (p, j) in pc_set) for p in mixed_block.phase_list)

        elif mb_type == MaterialBalanceType.elementTotal:
            raise ConfigurationError("{} Mixers do not support elemental "
                                     "material balances.".format(self.name))
        elif mb_type == MaterialBalanceType.none:
            pass
        else:
            raise BurntToast(
                "{} Mixer received unrecognised value for "
                "material_balance_type. This should not happen, "
                "please report this bug to the IDAES developers.".format(
                    self.name))

    def add_energy_mixing_equations(self, inlet_blocks, mixed_block):
        """
        Add energy mixing equations (total enthalpy balance).
        """
        @self.Constraint(self.flowsheet().time, doc="Energy balances")
        def enthalpy_mixing_equations(b, t):
            return 0 == (sum(
                sum(inlet_blocks[i][t].get_enthalpy_flow_terms(p)
                    for p in mixed_block.phase_list)
                for i in range(len(inlet_blocks))) -
                         sum(mixed_block[t].get_enthalpy_flow_terms(p)
                             for p in mixed_block.phase_list))

    def add_pressure_minimization_equations(self, inlet_blocks, mixed_block):
        """
        Add pressure minimization equations. This is done by sequential
        comparisons of each inlet to the minimum pressure so far, using
        the IDAES smooth minimum fuction.
        """
        if not hasattr(self, "inlet_idx"):
            self.inlet_idx = RangeSet(len(inlet_blocks))

        # Get units metadata
        units = self.config.property_package.get_metadata()

        # Add variables
        self.minimum_pressure = Var(
            self.flowsheet().time,
            self.inlet_idx,
            doc="Variable for calculating minimum inlet pressure",
            units=units.get_derived_units("pressure"))

        self.eps_pressure = Param(
            mutable=True,
            initialize=1e-3,
            domain=PositiveReals,
            doc="Smoothing term for minimum inlet pressure",
            units=units.get_derived_units("pressure"))

        # Calculate minimum inlet pressure
        @self.Constraint(
            self.flowsheet().time,
            self.inlet_idx,
            doc="Calculation for minimum inlet pressure",
        )
        def minimum_pressure_constraint(b, t, i):
            if i == self.inlet_idx.first():
                return self.minimum_pressure[t, i] == (
                    inlet_blocks[i - 1][t].pressure)
            else:
                return self.minimum_pressure[t, i] == (smooth_min(
                    self.minimum_pressure[t, i - 1],
                    inlet_blocks[i - 1][t].pressure,
                    self.eps_pressure,
                ))

        # Set inlet pressure to minimum pressure
        @self.Constraint(self.flowsheet().time,
                         doc="Link pressure to control volume")
        def mixture_pressure(b, t):
            return mixed_block[t].pressure == (
                self.minimum_pressure[t, self.inlet_idx.last()])

    def add_pressure_equality_equations(self, inlet_blocks, mixed_block):
        """
        Add pressure equality equations. Note that this writes a number of
        constraints equal to the number of inlets, enforcing equality between
        all inlets and the mixed stream.
        """
        if not hasattr(self, "inlet_idx"):
            self.inlet_idx = RangeSet(len(inlet_blocks))

        # Create equality constraints
        @self.Constraint(
            self.flowsheet().time,
            self.inlet_idx,
            doc="Calculation for minimum inlet pressure",
        )
        def pressure_equality_constraints(b, t, i):
            return mixed_block[t].pressure == inlet_blocks[i - 1][t].pressure

    def add_port_objects(self, inlet_list, inlet_blocks, mixed_block):
        """
        Adds Port objects if required.

        Args:
            a list of inlet StateBlock objects
            a mixed state StateBlock object

        Returns:
            None
        """
        if self.config.construct_ports is True:
            # Add ports
            for p in inlet_list:
                i_state = getattr(self, p + "_state")
                self.add_port(name=p, block=i_state, doc="Inlet Port")
            self.add_port(name="outlet", block=mixed_block, doc="Outlet Port")

    def model_check(blk):
        """
        This method executes the model_check methods on the associated state
        blocks (if they exist). This method is generally called by a unit model
        as part of the unit's model_check method.

        Args:
            None

        Returns:
            None
        """
        # Try property block model check
        for t in blk.flowsheet().time:
            try:
                inlet_list = blk.create_inlet_list()
                for i in inlet_list:
                    i_block = getattr(blk, i + "_state")
                    i_block[t].model_check()
            except AttributeError:
                _log.warning(
                    "{} Mixer inlet property block has no model "
                    "checks. To correct this, add a model_check "
                    "method to the associated StateBlock class.".format(
                        blk.name))
            try:
                if blk.config.mixed_state_block is None:
                    blk.mixed_state[t].model_check()
                else:
                    blk.config.mixed_state_block.model_check()
            except AttributeError:
                _log.warning("{} Mixer outlet property block has no "
                             "model checks. To correct this, add a "
                             "model_check method to the associated "
                             "StateBlock class.".format(blk.name))

    def use_minimum_inlet_pressure_constraint(self):
        """Activate the mixer pressure = mimimum inlet pressure constraint and
        deactivate the mixer pressure and all inlet pressures are equal
        constraints. This should only be used when momentum_mixing_type ==
        MomentumMixingType.minimize_and_equality.
        """
        if (self.config.momentum_mixing_type !=
                MomentumMixingType.minimize_and_equality):
            _log.warning(
                """use_minimum_inlet_pressure_constraint() can only be used
                when momentum_mixing_type ==
                MomentumMixingType.minimize_and_equality""")
            return
        self.minimum_pressure_constraint.activate()
        self.pressure_equality_constraints.deactivate()

    def use_equal_pressure_constraint(self):
        """Deactivate the mixer pressure = mimimum inlet pressure constraint
        and activate the mixer pressure and all inlet pressures are equal
        constraints. This should only be used when momentum_mixing_type ==
        MomentumMixingType.minimize_and_equality.
        """
        if (self.config.momentum_mixing_type !=
                MomentumMixingType.minimize_and_equality):
            _log.warning(
                """use_equal_pressure_constraint() can only be used when
                momentum_mixing_type ==
                MomentumMixingType.minimize_and_equality""")
            return
        self.minimum_pressure_constraint.deactivate()
        self.pressure_equality_constraints.activate()

    def initialize(blk,
                   outlvl=idaeslog.NOTSET,
                   optarg=None,
                   solver=None,
                   hold_state=False):
        """
        Initialization routine for mixer.

        Keyword Arguments:
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None, use
                     default solver options)
            solver : str indicating which solver to use during
                     initialization (default = None, use default solver)
            hold_state : flag indicating whether the initialization routine
                     should unfix any state variables fixed during
                     initialization, **default** - False. **Valid values:**
                     **True** - states variables are not unfixed, and a dict of
                     returned containing flags for which states were fixed
                     during initialization, **False** - state variables are
                     unfixed after initialization by calling the release_state
                     method.

        Returns:
            If hold_states is True, returns a dict containing flags for which
            states were fixed during initialization.
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        # Initialize inlet state blocks
        flags = {}
        inlet_list = blk.create_inlet_list()
        i_block_list = []
        for i in inlet_list:
            i_block = getattr(blk, i + "_state")
            i_block_list.append(i_block)
            flags[i] = {}
            flags[i] = i_block.initialize(
                outlvl=outlvl,
                optarg=optarg,
                solver=solver,
                hold_state=True,
            )

        # Initialize mixed state block
        if blk.config.mixed_state_block is None:
            mblock = blk.mixed_state
        else:
            mblock = blk.config.mixed_state_block

        o_flags = {}
        # Calculate initial guesses for mixed stream state
        for t in blk.flowsheet().time:
            # Iterate over state vars as defined by property package
            s_vars = mblock[t].define_state_vars()
            for s in s_vars:
                i_vars = []
                for k in s_vars[s]:
                    # Record whether variable was fixed or not
                    o_flags[t, s, k] = s_vars[s][k].fixed

                    # If fixed, use current value
                    # otherwise calculate guess from mixed state
                    if not s_vars[s][k].fixed:
                        for i in range(len(i_block_list)):
                            i_vars.append(
                                getattr(i_block_list[i][t],
                                        s_vars[s].local_name))

                        if s == "pressure":
                            # If pressure, use minimum as initial guess
                            mblock[t].pressure.value = min(
                                i_block_list[i][t].pressure.value
                                for i in range(len(i_block_list)))
                        elif "flow" in s:
                            # If a "flow" variable (i.e. extensive), sum inlets
                            for k in s_vars[s]:
                                s_vars[s][k].value = sum(
                                    i_vars[i][k].value
                                    for i in range(len(i_block_list)))
                        else:
                            # Otherwise use average of inlets
                            for k in s_vars[s]:
                                s_vars[s][k].value = sum(
                                    i_vars[i][k].value for i in range(
                                        len(i_block_list))) / len(i_block_list)

        mblock.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=False,
        )

        # Revert fixed status of variables to what they were before
        for t in blk.flowsheet().time:
            s_vars = mblock[t].define_state_vars()
            for s in s_vars:
                for k in s_vars[s]:
                    s_vars[s][k].fixed = o_flags[t, s, k]

        if blk.config.mixed_state_block is None:
            if (hasattr(blk, "pressure_equality_constraints")
                    and blk.pressure_equality_constraints.active is True):
                blk.pressure_equality_constraints.deactivate()
                for t in blk.flowsheet().time:
                    sys_press = getattr(blk,
                                        blk.create_inlet_list()[0] +
                                        "_state")[t].pressure
                    blk.mixed_state[t].pressure.fix(sys_press.value)
                with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
                    res = opt.solve(blk, tee=slc.tee)
                blk.pressure_equality_constraints.activate()
                for t in blk.flowsheet().time:
                    blk.mixed_state[t].pressure.unfix()
            else:
                with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
                    res = opt.solve(blk, tee=slc.tee)

            init_log.info("Initialization Complete: {}".format(
                idaeslog.condition(res)))
        else:
            init_log.info("Initialization Complete.")

        if hold_state is True:
            return flags
        else:
            blk.release_state(flags, outlvl=outlvl)

    def release_state(blk, flags, outlvl=idaeslog.NOTSET):
        """
        Method to release state variables fixed during initialization.

        Keyword Arguments:
            flags : dict containing information of which state variables
                    were fixed during initialization, and should now be
                    unfixed. This dict is returned by initialize if
                    hold_state = True.
            outlvl : sets output level of logging

        Returns:
            None
        """
        inlet_list = blk.create_inlet_list()
        for i in inlet_list:
            i_block = getattr(blk, i + "_state")
            i_block.release_state(flags[i], outlvl=outlvl)

    def _get_stream_table_contents(self, time_point=0):
        io_dict = {}
        inlet_list = self.create_inlet_list()
        for i in inlet_list:
            io_dict[i] = getattr(self, i + "_state")
        if self.config.mixed_state_block is None:
            io_dict["Outlet"] = self.mixed_state
        else:
            io_dict["Outlet"] = self.config.mixed_state_block
        return create_stream_table_dataframe(io_dict, time_point=time_point)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        mb_type = self.config.material_balance_type
        if mb_type == MaterialBalanceType.useDefault:
            t_ref = self.flowsheet().time.first()
            mb_type = self.mixed_state[t_ref].default_material_balance_type()

        if hasattr(self, "pressure_equality_constraints"):
            for (t, i), c in self.pressure_equality_constraints.items():
                s = iscale.get_scaling_factor(self.mixed_state[t].pressure,
                                              default=1,
                                              warning=True)
                iscale.constraint_scaling_transform(c, s)

        if hasattr(self, "material_mixing_equations"):
            if mb_type == MaterialBalanceType.componentPhase:
                for (t, p, j), c in self.material_mixing_equations.items():
                    flow_term = self.mixed_state[t].get_material_flow_terms(
                        p, j)
                    s = iscale.get_scaling_factor(flow_term, default=1)
                    iscale.constraint_scaling_transform(c, s, overwrite=False)
            elif mb_type == MaterialBalanceType.componentTotal:
                for (t, j), c in self.material_mixing_equations.items():
                    for i, p in enumerate(self.mixed_state.phase_list):
                        try:
                            ft = self.mixed_state[t].get_material_flow_terms(
                                p, j)
                        except (KeyError, AttributeError):
                            continue  # component not in phase
                        if i == 0:
                            s = iscale.get_scaling_factor(ft, default=1)
                        else:
                            _s = iscale.get_scaling_factor(ft, default=1)
                            s = _s if _s < s else s
                    iscale.constraint_scaling_transform(c, s, overwrite=False)
            elif mb_type == MaterialBalanceType.total:
                pc_set = self.mixed_state.phase_component_set
                for t, c in self.material_mixing_equations.items():
                    for i, (p, j) in enumerate(pc_set):
                        ft = self.mixed_state[t].get_material_flow_terms(p, j)
                        if i == 0:
                            s = iscale.get_scaling_factor(ft, default=1)
                        else:
                            _s = iscale.get_scaling_factor(ft, default=1)
                            s = _s if _s < s else s
                    iscale.constraint_scaling_transform(c, s, overwrite=False)

        if hasattr(self, "enthalpy_mixing_equations"):
            for t, c in self.enthalpy_mixing_equations.items():

                def scale_gen():
                    for v in self.mixed_state[t].phase_list:
                        yield self.mixed_state[t].get_enthalpy_flow_terms(p)

                s = iscale.min_scaling_factor(scale_gen(), default=1)
                iscale.constraint_scaling_transform(c, s, overwrite=False)
Esempio n. 9
0
class IntegerToBinary(IsomorphicTransformation):
    """Reformulate integer variables to binary variables and constraints.

    This transformation may be safely applied multiple times to the same model.
    """

    CONFIG = ConfigBlock("contrib.integer_to_binary")
    CONFIG.declare(
        "strategy",
        ConfigValue(
            default='base2',
            domain=In('base2', ),
            description="Reformulation method",
            # TODO: eventually we will support other methods, but not yet.
        ))
    CONFIG.declare(
        "ignore_unused",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "Ignore variables that do not appear in (potentially) active constraints. "
            "These variables are unlikely to be passed to the solver."))
    CONFIG.declare(
        "relax_integrality",
        ConfigValue(
            default=True,
            domain=bool,
            description="Relax the integrality of the integer variables "
            "after adding in the binary variables and constraints."))

    def _apply_to(self, model, **kwds):
        """Apply the transformation to the given model."""
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)

        integer_vars = list(v for v in model.component_data_objects(
            ctype=Var, descend_into=(Block, Disjunct))
                            if v.is_integer() and not v.fixed)
        if len(integer_vars) == 0:
            logger.info(
                "Model has no free integer variables. No reformulation needed."
            )
            return

        vars_on_constr = ComponentSet()
        for c in model.component_data_objects(ctype=Constraint,
                                              descend_into=(Block, Disjunct),
                                              active=True):
            vars_on_constr.update(
                v for v in identify_variables(c.body, include_fixed=False)
                if v.is_integer())

        if config.ignore_unused:
            num_vars_not_on_constr = len(integer_vars) - len(vars_on_constr)
            if num_vars_not_on_constr > 0:
                logger.info(
                    "%s integer variables on the model are not attached to any constraints. "
                    "Ignoring unused variables.")
            integer_vars = list(vars_on_constr)

        logger.info("Reformulating integer variables using the %s strategy." %
                    config.strategy)

        # Set up reformulation block
        blk_name = unique_component_name(model, "_int_to_binary_reform")
        reform_block = Block(
            doc="Holds variables and constraints for reformulating "
            "integer variables to binary variables.")
        setattr(model, blk_name, reform_block)

        reform_block.int_var_set = RangeSet(0, len(integer_vars) - 1)

        reform_block.new_binary_var = Var(
            Any,
            domain=Binary,
            dense=False,
            initialize=0,
            doc="Binary variable with index (int_var_idx, idx)")
        reform_block.integer_to_binary_constraint = Constraint(
            reform_block.int_var_set,
            doc="Equality constraints mapping the binary variable values "
            "to the integer variable value.")

        # check that variables are bounded
        for idx, int_var in enumerate(integer_vars):
            if not (int_var.has_lb() and int_var.has_ub()):
                raise ValueError(
                    "Integer variable %s is missing an "
                    "upper or lower bound. LB: %s; UB: %s. "
                    "Integer to binary reformulation does not support unbounded integer variables."
                    % (int_var.name, int_var.lb, int_var.ub))
            # do the reformulation
            highest_power = int(floor(log(value(int_var.ub - int_var.lb), 2)))
            # TODO potentially fragile due to floating point

            reform_block.integer_to_binary_constraint.add(
                idx,
                expr=int_var == sum(reform_block.new_binary_var[idx, pwr] *
                                    (2**pwr)
                                    for pwr in range(0, highest_power + 1)) +
                int_var.lb)

            # Relax the original integer variable
            if config.relax_integrality:
                int_var.domain = Reals

        logger.info("Reformulated %s integer variables using "
                    "%s binary variables and %s constraints." %
                    (len(integer_vars), len(reform_block.new_binary_var),
                     len(reform_block.integer_to_binary_constraint)))
Esempio n. 10
0
class CoagulationFlocculationData(UnitModelBlockData):
    """
    Zero order Coagulation-Flocculation model based on Jar Tests
    """
    # CONFIG are options for the unit model
    CONFIG = ConfigBlock()

    CONFIG.declare("dynamic", ConfigValue(
        domain=In([False]),
        default=False,
        description="Dynamic model flag - must be False",
        doc="""Indicates whether this model will be dynamic or not,
    **default** = False. The filtration unit does not support dynamic
    behavior, thus this must be False."""))

    CONFIG.declare("has_holdup", ConfigValue(
        default=False,
        domain=In([False]),
        description="Holdup construction flag - must be False",
        doc="""Indicates whether holdup terms should be constructed or not.
    **default** - False. The filtration unit does not have defined volume, thus
    this must be False."""))

    CONFIG.declare("material_balance_type", ConfigValue(
        default=MaterialBalanceType.useDefault,
        domain=In(MaterialBalanceType),
        description="Material balance construction flag",
        doc="""Indicates what type of mass balance should be constructed,
    **default** - MaterialBalanceType.useDefault.
    **Valid values:** {
    **MaterialBalanceType.useDefault - refer to property package for default
    balance type
    **MaterialBalanceType.none** - exclude material balances,
    **MaterialBalanceType.componentPhase** - use phase component balances,
    **MaterialBalanceType.componentTotal** - use total component balances,
    **MaterialBalanceType.elementTotal** - use total element balances,
    **MaterialBalanceType.total** - use total material balance.}"""))

    # NOTE: This option is temporarily disabled
    '''
    CONFIG.declare("energy_balance_type", ConfigValue(
        default=EnergyBalanceType.useDefault,
        domain=In(EnergyBalanceType),
        description="Energy balance construction flag",
        doc="""Indicates what type of energy balance should be constructed,
    **default** - EnergyBalanceType.useDefault.
    **Valid values:** {
    **EnergyBalanceType.useDefault - refer to property package for default
    balance type
    **EnergyBalanceType.none** - exclude energy balances,
    **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
    **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
    **EnergyBalanceType.energyTotal** - single energy balance for material,
    **EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    '''

    CONFIG.declare("momentum_balance_type", ConfigValue(
        default=MomentumBalanceType.pressureTotal,
        domain=In(MomentumBalanceType),
        description="Momentum balance construction flag",
        doc="""Indicates what type of momentum balance should be constructed,
    **default** - MomentumBalanceType.pressureTotal.
    **Valid values:** {
    **MomentumBalanceType.none** - exclude momentum balances,
    **MomentumBalanceType.pressureTotal** - single pressure balance for material,
    **MomentumBalanceType.pressurePhase** - pressure balances for each phase,
    **MomentumBalanceType.momentumTotal** - single momentum balance for material,
    **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))

    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,
    **PhysicalParameterObject** - a PhysicalParameterBlock 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("chemical_additives", ConfigValue(
        default={},
        domain=dict,
        description="""Dictionary of chemical additives used in coagulation process,
        along with their molecular weights, the moles of salt produced per mole of
        chemical added, and the molecular weights of the salt produced by the chemical
        additive with the format of: \n
            {'chem_name_1':
                {'parameter_data':
                    {
                    'mw_additive': (value, units),
                    'moles_salt_per_mole_additive': value,
                    'mw_salt': (value, units)
                    }
                },
            'chem_name_2':
                {'parameter_data':
                    {
                    'mw_additive': (value, units),
                    'moles_salt_per_mole_additive': value,
                    'mw_salt': (value, units)
                    }
                },
            } """))


    def build(self):
        # build always starts by calling super().build()
        # This triggers a lot of boilerplate in the background for you
        super().build()

        # this creates blank scaling factors, which are populated later
        self.scaling_factor = Suffix(direction=Suffix.EXPORT)

        # Next, get the base units of measurement from the property definition
        units_meta = self.config.property_package.get_metadata().get_derived_units

        # check the optional config arg 'chemical_additives'
        common_msg = "The 'chemical_additives' dict MUST contain a dict of 'parameter_data' for " + \
                     "each chemical name. That 'parameter_data' dict MUST contain 'mw_chem', " + \
                     "'moles_salt_per_mole_additive', and 'mw_salt' as keys. Users are also " + \
                     "required to provide the values for the molecular weights and the units " + \
                     "within a tuple arg. Example format provided below.\n\n" + \
                     "{'chem_name_1': \n" + \
                     "     {'parameter_data': \n" + \
                     "        {'mw_additive': (value, units), \n" + \
                     "         'moles_salt_per_mole_additive': value, \n" + \
                     "         'mw_salt': (value, units)} \n" + \
                     "     }, \n" + \
                     "}\n\n"
        mw_adds = {}
        mw_salts = {}
        molar_rat = {}
        for j in self.config.chemical_additives:
            if type(self.config.chemical_additives[j]) != dict:
                raise ConfigurationError("\n Did not provide a 'dict' for chemical \n" + common_msg)
            if 'parameter_data' not in self.config.chemical_additives[j]:
                raise ConfigurationError("\n Did not provide a 'parameter_data' for chemical \n" + common_msg)
            if 'mw_additive' not in self.config.chemical_additives[j]['parameter_data']:
                raise ConfigurationError("\n Did not provide a 'mw_additive' for chemical \n" + common_msg)
            if 'moles_salt_per_mole_additive' not in self.config.chemical_additives[j]['parameter_data']:
                raise ConfigurationError("\n Did not provide a 'moles_salt_per_mole_additive' for chemical \n" + common_msg)
            if 'mw_salt' not in self.config.chemical_additives[j]['parameter_data']:
                raise ConfigurationError("\n Did not provide a 'mw_salt' for chemical \n" + common_msg)
            if type(self.config.chemical_additives[j]['parameter_data']['mw_additive']) != tuple:
                raise ConfigurationError("\n Did not provide a tuple for 'mw_additive' \n" + common_msg)
            if type(self.config.chemical_additives[j]['parameter_data']['mw_salt']) != tuple:
                raise ConfigurationError("\n Did not provide a tuple for 'mw_salt' \n" + common_msg)
            if not isinstance(self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive'], (int,float)):
                raise ConfigurationError("\n Did not provide a number for 'moles_salt_per_mole_additive' \n" + common_msg)

            #Populate temp dicts for parameter and variable setting
            mw_adds[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_additive'][0],
                        from_units=self.config.chemical_additives[j]['parameter_data']['mw_additive'][1], to_units=pyunits.kg/pyunits.mol)
            mw_salts[j] = pyunits.convert_value(self.config.chemical_additives[j]['parameter_data']['mw_salt'][0],
                        from_units=self.config.chemical_additives[j]['parameter_data']['mw_salt'][1], to_units=pyunits.kg/pyunits.mol)
            molar_rat[j] = self.config.chemical_additives[j]['parameter_data']['moles_salt_per_mole_additive']

        # Add unit variables
        # Linear relationship between TSS (mg/L) and Turbidity (NTU)
        #           TSS (mg/L) = Turbidity (NTU) * slope + intercept
        #   Default values come from the following paper:
        #       H. Rugner, M. Schwientek,B. Beckingham, B. Kuch, P. Grathwohl,
        #       Environ. Earth Sci. 69 (2013) 373-380. DOI: 10.1007/s12665-013-2307-1
        self.slope = Var(
            self.flowsheet().config.time,
            initialize=1.86,
            bounds=(1e-8, 10),
            domain=NonNegativeReals,
            units=pyunits.mg/pyunits.L,
            doc='Slope relation between TSS (mg/L) and Turbidity (NTU)')

        self.intercept = Var(
            self.flowsheet().config.time,
            initialize=0,
            bounds=(0, 10),
            domain=NonNegativeReals,
            units=pyunits.mg/pyunits.L,
            doc='Intercept relation between TSS (mg/L) and Turbidity (NTU)')

        self.initial_turbidity_ntu = Var(
            self.flowsheet().config.time,
            initialize=50,
            bounds=(0, 10000),
            domain=NonNegativeReals,
            units=pyunits.dimensionless,
            doc='Initial measured Turbidity (NTU) from Jar Test')

        self.final_turbidity_ntu = Var(
            self.flowsheet().config.time,
            initialize=1,
            bounds=(0, 10000),
            domain=NonNegativeReals,
            units=pyunits.dimensionless,
            doc='Final measured Turbidity (NTU) from Jar Test')

        self.chemical_doses = Var(
            self.flowsheet().config.time,
            self.config.chemical_additives.keys(),
            initialize=0,
            bounds=(0, 100),
            domain=NonNegativeReals,
            units=pyunits.mg/pyunits.L,
            doc='Dosages of the set of chemical additives')

        self.chemical_mw = Param(
            self.config.chemical_additives.keys(),
            mutable=True,
            initialize=mw_adds,
            domain=NonNegativeReals,
            units=pyunits.kg/pyunits.mol,
            doc='Molecular weights of the set of chemical additives')

        self.salt_mw = Param(
            self.config.chemical_additives.keys(),
            mutable=True,
            initialize=mw_salts,
            domain=NonNegativeReals,
            units=pyunits.kg/pyunits.mol,
            doc='Molecular weights of the produced salts from chemical additives')

        self.salt_from_additive_mole_ratio = Param(
            self.config.chemical_additives.keys(),
            mutable=True,
            initialize=molar_rat,
            domain=NonNegativeReals,
            units=pyunits.mol/pyunits.mol,
            doc='Moles of the produced salts from 1 mole of chemical additives')


        # Build control volume for feed side
        self.control_volume = ControlVolume0DBlock(default={
            "dynamic": False,
            "has_holdup": False,
            "property_package": self.config.property_package,
            "property_package_args": self.config.property_package_args})

        self.control_volume.add_state_blocks(
            has_phase_equilibrium=False)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_mass_transfer=True)

        # NOTE: This checks for if an energy_balance_type is defined
        if hasattr(self.config, "energy_balance_type"):
            self.control_volume.add_energy_balances(
                balance_type=self.config.energy_balance_type,
                has_enthalpy_transfer=False)

        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=False)

        # Add ports
        self.add_inlet_port(name='inlet', block=self.control_volume)
        self.add_outlet_port(name='outlet', block=self.control_volume)

        # Check _phase_component_set for required items
        if ('Liq', 'TDS') not in self.config.property_package._phase_component_set:
            raise ConfigurationError(
                "Coagulation-Flocculation model MUST contain ('Liq','TDS') as a component, but "
                "the property package has only specified the following components {}"
                    .format([p for p in self.config.property_package._phase_component_set]))
        if ('Liq', 'Sludge') not in self.config.property_package._phase_component_set:
            raise ConfigurationError(
                "Coagulation-Flocculation model MUST contain ('Liq','Sludge') as a component, but "
                "the property package has only specified the following components {}"
                    .format([p for p in self.config.property_package._phase_component_set]))
        if ('Liq', 'TSS') not in self.config.property_package._phase_component_set:
            raise ConfigurationError(
                "Coagulation-Flocculation model MUST contain ('Liq','TSS') as a component, but "
                "the property package has only specified the following components {}"
                    .format([p for p in self.config.property_package._phase_component_set]))

        # -------- Add constraints ---------
        # Adds isothermal constraint if no energy balance present
        if not hasattr(self.config, "energy_balance_type"):
            @self.Constraint(self.flowsheet().config.time,
                             doc="Isothermal condition")
            def eq_isothermal(self, t):
                return (self.control_volume.properties_out[t].temperature == self.control_volume.properties_in[t].temperature)

        # Constraint for tss loss rate based on measured final turbidity
        self.tss_loss_rate = Var(
            self.flowsheet().config.time,
            initialize=1,
            bounds=(0, 100),
            domain=NonNegativeReals,
            units=units_meta('mass')*units_meta('time')**-1,
            doc='Mass per time loss rate of TSS based on the measured final turbidity')

        @self.Constraint(self.flowsheet().config.time,
                         doc="Constraint for the loss rate of TSS to be used in mass_transfer_term")
        def eq_tss_loss_rate(self, t):
            tss_out = pyunits.convert(self.slope[t]*self.final_turbidity_ntu[t] + self.intercept[t],
                                    to_units=units_meta('mass')*units_meta('length')**-3)
            input_rate = self.control_volume.properties_in[t].flow_mass_phase_comp['Liq','TSS']
            exit_rate = self.control_volume.properties_out[t].flow_vol_phase['Liq']*tss_out

            return (self.tss_loss_rate[t] == input_rate - exit_rate)

        # Constraint for tds gain rate based on 'chemical_doses' and 'chemical_additives'
        if self.config.chemical_additives:
            self.tds_gain_rate = Var(
                self.flowsheet().config.time,
                initialize=0,
                bounds=(0, 100),
                domain=NonNegativeReals,
                units=units_meta('mass')*units_meta('time')**-1,
                doc='Mass per time gain rate of TDS based on the chemicals added for coagulation')

            @self.Constraint(self.flowsheet().config.time,
                             doc="Constraint for the loss rate of TSS to be used in mass_transfer_term")
            def eq_tds_gain_rate(self, t):
                sum = 0
                for j in self.config.chemical_additives.keys():
                    chem_dose = pyunits.convert(self.chemical_doses[t, j],
                                    to_units=units_meta('mass')*units_meta('length')**-3)
                    chem_dose = chem_dose/self.chemical_mw[j] * \
                            self.salt_from_additive_mole_ratio[j] * \
                            self.salt_mw[j]*self.control_volume.properties_out[t].flow_vol_phase['Liq']
                    sum = sum+chem_dose

                return (self.tds_gain_rate[t] == sum)

        # Add constraints for mass transfer terms
        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer term")
        def eq_mass_transfer_term(self, t, p, j):
            if (p, j) == ('Liq', 'TSS'):
                return self.control_volume.mass_transfer_term[t, p, j] == -self.tss_loss_rate[t]
            elif (p, j) == ('Liq', 'Sludge'):
                return self.control_volume.mass_transfer_term[t, p, j] == self.tss_loss_rate[t]
            elif (p, j) == ('Liq', 'TDS'):
                if self.config.chemical_additives:
                    return self.control_volume.mass_transfer_term[t, p, j] == self.tds_gain_rate[t]
                else:
                    return self.control_volume.mass_transfer_term[t, p, j] == 0.0
            else:
                return self.control_volume.mass_transfer_term[t, p, j] == 0.0

    # Return a scalar expression for the inlet concentration of TSS
    def compute_inlet_tss_mass_concentration(self, t):
        """
        Function to generate an expression that would represent the mass
        concentration of TSS at the inlet port of the unit. Inlet ports
        are generally established upstream, but this will be useful for
        establishing the inlet TSS when an upstream TSS is unknown. This
        level of inlet TSS is based off of measurements made of Turbidity
        during the Jar Test.

        Keyword Arguments:
            self : this unit model object
            t : time index on the flowsheet

        Returns: Expression

        Recover the numeric value by using 'value(Expression)'
        """
        units_meta = self.config.property_package.get_metadata().get_derived_units
        return pyunits.convert(self.slope[t]*self.initial_turbidity_ntu[t] + self.intercept[t],
                                to_units=units_meta('mass')*units_meta('length')**-3)

    # Return a scale expression for the inlet mass flow rate of TSS
    def compute_inlet_tss_mass_flow(self, t):
        """
        Function to generate an expression that would represent the mass
        flow rate of TSS at the inlet port of the unit. Inlet ports
        are generally established upstream, but this will be useful for
        establishing the inlet TSS when an upstream TSS is unknown. This
        level of inlet TSS is based off of measurements made of Turbidity
        during the Jar Test.

        Keyword Arguments:
            self : this unit model object
            t : time index on the flowsheet

        Returns: Expression

        Recover the numeric value by using 'value(Expression)'
        """
        return self.control_volume.properties_in[t].flow_vol_phase['Liq']*self.compute_inlet_tss_mass_concentration(t)

    # Function to automate fixing of the Turbidity v TSS relation params to defaults
    def fix_tss_turbidity_relation_defaults(self):
        self.slope.fix()
        self.intercept.fix()

    # initialize method
    def initialize_build(
            blk,
            state_args=None,
            outlvl=idaeslog.NOTSET,
            solver=None,
            optarg=None):
        """
        General wrapper for pressure changer initialization routines

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None)
            solver : str indicating which solver to use during
                     initialization (default = None)

        Returns: None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
        # Set solver options
        opt = get_solver(solver, optarg)

        # ---------------------------------------------------------------------
        # Initialize holdup block
        flags = blk.control_volume.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )
        init_log.info_high("Initialization Step 1 Complete.")
        # ---------------------------------------------------------------------

        # ---------------------------------------------------------------------
        # Solve unit
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high(
            "Initialization Step 2 {}.".format(idaeslog.condition(res)))

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.control_volume.release_state(flags, outlvl + 1)
        init_log.info(
            "Initialization Complete: {}".format(idaeslog.condition(res))
        )

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        units_meta = self.config.property_package.get_metadata().get_derived_units

        # scaling factors for turbidity relationship
        #       Supressing warning (these factors are not very important)
        if iscale.get_scaling_factor(self.slope) is None:
            sf = iscale.get_scaling_factor(self.slope, default=1, warning=False)
            iscale.set_scaling_factor(self.slope, sf)
        if iscale.get_scaling_factor(self.intercept) is None:
            sf = iscale.get_scaling_factor(self.intercept, default=1, warning=False)
            iscale.set_scaling_factor(self.intercept, sf)

        # scaling factors for turbidity measurements and chemical doses
        #       Supressing warning
        if iscale.get_scaling_factor(self.initial_turbidity_ntu) is None:
            sf = iscale.get_scaling_factor(self.initial_turbidity_ntu, default=1, warning=False)
            iscale.set_scaling_factor(self.initial_turbidity_ntu, sf)
        if iscale.get_scaling_factor(self.final_turbidity_ntu) is None:
            sf = iscale.get_scaling_factor(self.final_turbidity_ntu, default=1, warning=False)
            iscale.set_scaling_factor(self.final_turbidity_ntu, sf)
        if iscale.get_scaling_factor(self.chemical_doses) is None:
            sf = iscale.get_scaling_factor(self.chemical_doses, default=1, warning=False)
            iscale.set_scaling_factor(self.chemical_doses, sf)

        # set scaling for tss_loss_rate
        if iscale.get_scaling_factor(self.tss_loss_rate) is None:
            sf = 0
            for t in self.control_volume.properties_in:
                sf += value(self.control_volume.properties_in[t].flow_mass_phase_comp['Liq','TSS'])
            sf = sf / len(self.control_volume.properties_in)
            if sf < 0.01:
                sf = 0.01
            iscale.set_scaling_factor(self.tss_loss_rate, 1/sf)

            for ind, c in self.eq_tss_loss_rate.items():
                iscale.constraint_scaling_transform(c, 1/sf)

        # set scaling for tds_gain_rate
        if self.config.chemical_additives:
            if iscale.get_scaling_factor(self.tds_gain_rate) is None:
                sf = 0
                for t in self.control_volume.properties_in:
                    sum = 0
                    for j in self.config.chemical_additives.keys():
                        chem_dose = pyunits.convert(self.chemical_doses[t, j],
                                        to_units=units_meta('mass')*units_meta('length')**-3)
                        chem_dose = chem_dose/self.chemical_mw[j] * \
                                self.salt_from_additive_mole_ratio[j] * \
                                self.salt_mw[j]*self.control_volume.properties_in[t].flow_vol_phase['Liq']
                        sum = sum+chem_dose
                    sf += value(sum)
                sf = sf / len(self.control_volume.properties_in)
                if sf < 0.001:
                    sf = 0.001
                iscale.set_scaling_factor(self.tds_gain_rate, 1/sf)

                for ind, c in self.eq_tds_gain_rate.items():
                    iscale.constraint_scaling_transform(c, 1/sf)

        # set scaling for mass transfer terms
        for ind, c in self.eq_mass_transfer_term.items():
            if ind[2] == "TDS":
                if self.config.chemical_additives:
                    sf = iscale.get_scaling_factor(self.tds_gain_rate)
                else:
                    sf = 1
            elif ind[2] == "TSS":
                sf = iscale.get_scaling_factor(self.tss_loss_rate)
            elif ind[2] == "Sludge":
                sf = iscale.get_scaling_factor(self.tss_loss_rate)
            else:
                sf = 1
            iscale.constraint_scaling_transform(c, sf)
            iscale.set_scaling_factor(self.control_volume.mass_transfer_term[ind] , sf)

        # set scaling factors for control_volume.properties_in based on control_volume.properties_out
        for t in self.control_volume.properties_in:
            if iscale.get_scaling_factor(self.control_volume.properties_in[t].dens_mass_phase) is None:
                sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].dens_mass_phase)
                iscale.set_scaling_factor(self.control_volume.properties_in[t].dens_mass_phase, sf)

            if iscale.get_scaling_factor(self.control_volume.properties_in[t].flow_mass_phase_comp) is None:
                for ind in self.control_volume.properties_in[t].flow_mass_phase_comp:
                    sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind])
                    iscale.set_scaling_factor(self.control_volume.properties_in[t].flow_mass_phase_comp[ind], sf)

            if iscale.get_scaling_factor(self.control_volume.properties_in[t].mass_frac_phase_comp) is None:
                for ind in self.control_volume.properties_in[t].mass_frac_phase_comp:
                    sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind])
                    iscale.set_scaling_factor(self.control_volume.properties_in[t].mass_frac_phase_comp[ind], sf)

            if iscale.get_scaling_factor(self.control_volume.properties_in[t].flow_vol_phase) is None:
                for ind in self.control_volume.properties_in[t].flow_vol_phase:
                    sf = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_vol_phase[ind])
                    iscale.set_scaling_factor(self.control_volume.properties_in[t].flow_vol_phase[ind], sf)

        # update scaling for control_volume.properties_out
        for t in self.control_volume.properties_out:
            if iscale.get_scaling_factor(self.control_volume.properties_out[t].dens_mass_phase) is None:
                iscale.set_scaling_factor(self.control_volume.properties_out[t].dens_mass_phase, 1e-3)

            # need to update scaling factors for TSS, Sludge, and TDS to account for the
            #   expected change in their respective values from the loss/gain rates
            for ind in self.control_volume.properties_out[t].flow_mass_phase_comp:
                if ind[1] == "TSS":
                    sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind])
                    sf_new = iscale.get_scaling_factor(self.tss_loss_rate)
                    iscale.set_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind], 100*sf_new*(sf_new/sf_og))
                if ind[1] == "Sludge":
                    sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind])
                    sf_new = iscale.get_scaling_factor(self.tss_loss_rate)
                    iscale.set_scaling_factor(self.control_volume.properties_out[t].flow_mass_phase_comp[ind], 100*sf_new*(sf_new/sf_og))

            for ind in self.control_volume.properties_out[t].mass_frac_phase_comp:
                if ind[1] == "TSS":
                    sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind])
                    sf_new = iscale.get_scaling_factor(self.tss_loss_rate)
                    iscale.set_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind], 100*sf_new*(sf_new/sf_og))
                if ind[1] == "Sludge":
                    sf_og = iscale.get_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind])
                    sf_new = iscale.get_scaling_factor(self.tss_loss_rate)
                    iscale.set_scaling_factor(self.control_volume.properties_out[t].mass_frac_phase_comp[ind], 100*sf_new*(sf_new/sf_og))
Esempio n. 11
0
class BigM_Transformation(Transformation):
    """Relax disjunctive model using big-M terms.

    Relaxes a disjunctive model into an algebraic model by adding Big-M
    terms to all disjunctive constraints.

    This transformation accepts the following keyword arguments:
        bigM: A user-specified value (or dict) of M values to use (see below)
        targets: the targets to transform [default: the instance]

    M values are determined as follows:
       1) if the constraint appears in the bigM argument dict
       2) if the constraint parent_component appears in the bigM
          argument dict
       3) if any block which is an ancestor to the constraint appears in 
          the bigM argument dict
       3) if 'None' is in the bigM argument dict
       4) if the constraint or the constraint parent_component appear in
          a BigM Suffix attached to any parent_block() beginning with the
          constraint's parent_block and moving up to the root model.
       5) if None appears in a BigM Suffix attached to any
          parent_block() between the constraint and the root model.
       6) if the constraint is linear, estimate M using the variable bounds

    M values may be a single value or a 2-tuple specifying the M for the
    lower bound and the upper bound of the constraint body.

    Specifying "bigM=N" is automatically mapped to "bigM={None: N}".

    The transformation will create a new Block with a unique
    name beginning "_pyomo_gdp_bigm_relaxation".  That Block will
    contain an indexed Block named "relaxedDisjuncts", which will hold
    the relaxed disjuncts.  This block is indexed by an integer
    indicating the order in which the disjuncts were relaxed.
    Each block has a dictionary "_constraintMap":
    
        'srcConstraints': ComponentMap(<transformed constraint>:
                                       <src constraint>)
        'transformedConstraints': ComponentMap(<src constraint>:
                                               <transformed constraint>)

    All transformed Disjuncts will have a pointer to the block their transformed
    constraints are on, and all transformed Disjunctions will have a 
    pointer to the corresponding OR or XOR constraint.

    """

    CONFIG = ConfigBlock("gdp.bigm")
    CONFIG.declare('targets', ConfigValue(
        default=None,
        domain=target_list,
        description="target or list of targets that will be relaxed",
        doc="""

        This specifies the list of components to relax. If None (default), the
        entire model is transformed. Note that if the transformation is done out
        of place, the list of targets should be attached to the model before it
        is cloned, and the list will specify the targets on the cloned
        instance."""
    ))
    CONFIG.declare('bigM', ConfigValue(
        default=None,
        domain=_to_dict,
        description="Big-M value used for constraint relaxation",
        doc="""

        A user-specified value, dict, or ComponentMap of M values that override
        M-values found through model Suffixes or that would otherwise be
        calculated using variable domains."""
    ))

    def __init__(self):
        """Initialize transformation object."""
        super(BigM_Transformation, self).__init__()
        self.handlers = {
            Constraint:  self._transform_constraint,
            Var:         False, # Note that if a Var appears on a Disjunct, we
                                # still treat its bounds as global. If the
                                # intent is for its bounds to be on the
                                # disjunct, it should be declared with no bounds
                                # and the bounds should be set in constraints on
                                # the Disjunct.
            Connector:   False,
            Expression:  False,
            Suffix:      False,
            Param:       False,
            Set:         False,
            RangeSet:    False,
            Disjunction: self._warn_for_active_disjunction,
            Disjunct:    self._warn_for_active_disjunct,
            Block:       self._transform_block_on_disjunct,
        }

    def _get_bigm_suffix_list(self, block):
        # Note that you can only specify suffixes on BlockData objects or
        # SimpleBlocks. Though it is possible at this point to stick them
        # on whatever components you want, we won't pick them up.
        suffix_list = []
        while block is not None:
            bigm = block.component('BigM')
            if type(bigm) is Suffix:
                suffix_list.append(bigm)
            block = block.parent_block()
        return suffix_list

    def _get_bigm_arg_list(self, bigm_args, block):
        # Gather what we know about blocks from args exactly once. We'll still
        # check for constraints in the moment, but if that fails, we've
        # preprocessed the time-consuming part of traversing up the tree.
        arg_list = []
        if bigm_args is None:
            return arg_list
        while block is not None:
            if block in bigm_args:
                arg_list.append({block: bigm_args[block]})
            block = block.parent_block()
        return arg_list

    def _apply_to(self, instance, **kwds):
        assert not NAME_BUFFER
        self.used_args = ComponentMap() # If everything was sure to go well,
                                        # this could be a dictionary. But if
                                        # someone messes up and gives us a Var
                                        # as a key in bigMargs, I need the error
                                        # not to be when I try to put it into
                                        # this map!
        try:
            self._apply_to_impl(instance, **kwds)
        finally:
            # Clear the global name buffer now that we are done
            NAME_BUFFER.clear()
            # same for our bookkeeping about what we used from bigM arg dict
            self.used_args.clear()
    
    def _apply_to_impl(self, instance, **kwds):
        config = self.CONFIG(kwds.pop('options', {}))

        # We will let args override suffixes and estimate as a last
        # resort. More specific args/suffixes override ones anywhere in
        # the tree. Suffixes lower down in the tree override ones higher
        # up.
        if 'default_bigM' in kwds:
            logger.warn("DEPRECATED: the 'default_bigM=' argument has been "
                        "replaced by 'bigM='")
            config.bigM = kwds.pop('default_bigM')

        config.set_value(kwds)
        bigM = config.bigM

        targets = config.targets
        if targets is None:
            targets = (instance, )
            _HACK_transform_whole_instance = True
        else:
            _HACK_transform_whole_instance = False
        # We need to check that all the targets are in fact on instance. As we
        # do this, we will use the set below to cache components we know to be
        # in the tree rooted at instance.
        knownBlocks = {}
        for t in targets:
            # check that t is in fact a child of instance
            if not is_child_of(parent=instance, child=t,
                               knownBlocks=knownBlocks):
                raise GDP_Error("Target %s is not a component on instance %s!"
                                % (t.name, instance.name))
            elif t.ctype is Disjunction:
                if t.parent_component() is t:
                    self._transform_disjunction(t, bigM)
                else:
                    self._transform_disjunctionData( t, bigM, t.index())
            elif t.ctype in (Block, Disjunct):
                if t.parent_component() is t:
                    self._transform_block(t, bigM)
                else:
                    self._transform_blockData(t, bigM)
            else:
                raise GDP_Error(
                    "Target %s was not a Block, Disjunct, or Disjunction. "
                    "It was of type %s and can't be transformed."
                    % (t.name, type(t)))

        # issue warnings about anything that was in the bigM args dict that we
        # didn't use
        if bigM is not None:
            unused_args = ComponentSet(bigM.keys()) - \
                          ComponentSet(self.used_args.keys())
            if len(unused_args) > 0:
                warning_msg = ("Unused arguments in the bigM map! "
                               "These arguments were not used by the "
                               "transformation:\n")
                for component in unused_args:
                    if hasattr(component, 'name'):
                        warning_msg += "\t%s\n" % component.name
                    else:
                        warning_msg += "\t%s\n" % component
                logger.warn(warning_msg)

        # HACK for backwards compatibility with the older GDP transformations
        #
        # Until the writers are updated to find variables on things
        # other than active blocks, we need to reclassify the Disjuncts
        # as Blocks after transformation so that the writer will pick up
        # all the variables that it needs (in this case, indicator_vars).
        if _HACK_transform_whole_instance:
            HACK_GDP_Disjunct_Reclassifier().apply_to(instance)

    def _add_transformation_block(self, instance):
        # make a transformation block on instance to put transformed disjuncts
        # on
        transBlockName = unique_component_name(
            instance,
            '_pyomo_gdp_bigm_relaxation')
        transBlock = Block()
        instance.add_component(transBlockName, transBlock)
        transBlock.relaxedDisjuncts = Block(Any)
        transBlock.lbub = Set(initialize=['lb', 'ub'])

        return transBlock

    def _transform_block(self, obj, bigM):
        for i in sorted(iterkeys(obj)):
            self._transform_blockData(obj[i], bigM)

    def _transform_blockData(self, obj, bigM):
        # Transform every (active) disjunction in the block
        for disjunction in obj.component_objects(
                Disjunction,
                active=True,
                sort=SortComponents.deterministic,
                descend_into=(Block, Disjunct),
                descent_order=TraversalStrategy.PostfixDFS):
            self._transform_disjunction(disjunction, bigM)

    def _add_xor_constraint(self, disjunction, transBlock):
        # Put the disjunction constraint on the transformation block and
        # determine whether it is an OR or XOR constraint.

        # We never do this for just a DisjunctionData because we need to know
        # about the index set of its parent component (so that we can make the
        # index of this constraint match). So if we called this on a
        # DisjunctionData, we did something wrong.
        assert isinstance(disjunction, Disjunction)

        # first check if the constraint already exists
        if not disjunction._algebraic_constraint is None:
            return disjunction._algebraic_constraint()

        # add the XOR (or OR) constraints to parent block (with unique name)
        # It's indexed if this is an IndexedDisjunction, not otherwise
        orC = Constraint(disjunction.index_set()) if \
            disjunction.is_indexed() else Constraint()
        # The name used to indicate if there were OR or XOR disjunctions,
        # however now that Disjunctions are allowed to mix the state we
        # can no longer make that distinction in the name.
        #    nm = '_xor' if xor else '_or'
        nm = '_xor'
        orCname = unique_component_name( transBlock, disjunction.getname(
            fully_qualified=True, name_buffer=NAME_BUFFER) + nm)
        transBlock.add_component(orCname, orC)
        disjunction._algebraic_constraint = weakref_ref(orC)

        return orC

    def _transform_disjunction(self, obj, bigM):
        if not obj.active:
            return

        # if this is an IndexedDisjunction we have seen in a prior call to the
        # transformation, we already have a transformation block for it. We'll
        # use that.
        if obj._algebraic_constraint is not None:
            transBlock = obj._algebraic_constraint().parent_block()
        else:
            transBlock = self._add_transformation_block(obj.parent_block())

        # If this is an IndexedDisjunction, we have to create the XOR constraint
        # here because we want its index to match the disjunction. In any case,
        # we might as well.
        xorConstraint = self._add_xor_constraint(obj, transBlock)

        # relax each of the disjunctionDatas
        for i in sorted(iterkeys(obj)):
            self._transform_disjunctionData(obj[i], bigM, i, xorConstraint,
                                            transBlock)

        # deactivate so the writers don't scream
        obj.deactivate()

    def _transform_disjunctionData(self, obj, bigM, index, xorConstraint=None,
                                   transBlock=None):
        if not obj.active:
            return  # Do not process a deactivated disjunction 
        # We won't have these arguments if this got called straight from
        # targets. But else, we created them earlier, and have just been passing
        # them through.
        if transBlock is None:
            # It's possible that we have already created a transformation block
            # for another disjunctionData from this same container. If that's
            # the case, let's use the same transformation block. (Else it will
            # be really confusing that the XOR constraint goes to that old block
            # but we create a new one here.)
            if not obj.parent_component()._algebraic_constraint is None:
                transBlock = obj.parent_component()._algebraic_constraint().\
                             parent_block()
            else:
                transBlock = self._add_transformation_block(obj.parent_block())
        if xorConstraint is None:
            xorConstraint = self._add_xor_constraint(obj.parent_component(),
                                                     transBlock)

        xor = obj.xor
        or_expr = 0
        # Just because it's unlikely this is what someone meant to do...    
        if len(obj.disjuncts) == 0:
            raise GDP_Error("Disjunction %s is empty. This is " 
                            "likely indicative of a modeling error."  %
                            obj.getname(fully_qualified=True,
                                        name_buffer=NAME_BUFFER))
        for disjunct in obj.disjuncts:
            or_expr += disjunct.indicator_var
            # make suffix list. (We don't need it until we are
            # transforming constraints, but it gets created at the
            # disjunct level, so more efficient to make it here and
            # pass it down.)
            suffix_list = self._get_bigm_suffix_list(disjunct)
            arg_list = self._get_bigm_arg_list(bigM, disjunct)
            # relax the disjunct
            self._transform_disjunct(disjunct, transBlock, bigM, arg_list,
                                     suffix_list)

        # add or (or xor) constraint
        if xor:
            xorConstraint[index] = or_expr == 1
        else:
            xorConstraint[index] = or_expr >= 1
        # Mark the DisjunctionData as transformed by mapping it to its XOR
        # constraint.
        obj._algebraic_constraint = weakref_ref(xorConstraint[index])
        
        # and deactivate for the writers
        obj.deactivate()

    def _transform_disjunct(self, obj, transBlock, bigM, arg_list, suffix_list):
        # deactivated -> either we've already transformed or user deactivated
        if not obj.active:
            if obj.indicator_var.is_fixed():
                if value(obj.indicator_var) == 0:
                    # The user cleanly deactivated the disjunct: there
                    # is nothing for us to do here.
                    return
                else:
                    raise GDP_Error(
                        "The disjunct %s is deactivated, but the "
                        "indicator_var is fixed to %s. This makes no sense."
                        % ( obj.name, value(obj.indicator_var) ))
            if obj._transformation_block is None:
                raise GDP_Error(
                    "The disjunct %s is deactivated, but the "
                    "indicator_var is not fixed and the disjunct does not "
                    "appear to have been relaxed. This makes no sense. "
                    "(If the intent is to deactivate the disjunct, fix its "
                    "indicator_var to 0.)"
                    % ( obj.name, ))
                
        if not obj._transformation_block is None:
            # we've transformed it, which means this is the second time it's
            # appearing in a Disjunction
            raise GDP_Error(
                    "The disjunct %s has been transformed, but a disjunction "
                    "it appears in has not. Putting the same disjunct in "
                    "multiple disjunctions is not supported." % obj.name)

        # add reference to original disjunct on transformation block
        relaxedDisjuncts = transBlock.relaxedDisjuncts
        relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)]
        # we will keep a map of constraints (hashable, ha!) to a tuple to
        # indicate where their m value came from, either (arg dict, key) if it
        # came from args, (Suffix, key) if it came from Suffixes, or (M_lower,
        # M_upper) if we calcualted it ourselves. I am keeping it here because I
        # want it to move with the disjunct transformation blocks in the case of
        # nested constraints, to make it easier to query.
        relaxationBlock.bigm_src = {}
        obj._transformation_block = weakref_ref(relaxationBlock)
        relaxationBlock._srcDisjunct = weakref_ref(obj)

        # This is crazy, but if the disjunction has been previously
        # relaxed, the disjunct *could* be deactivated.  This is a big
        # deal for CHull, as it uses the component_objects /
        # component_data_objects generators.  For BigM, that is OK,
        # because we never use those generators with active=True.  I am
        # only noting it here for the future when someone (me?) is
        # comparing the two relaxations.
        #
        # Transform each component within this disjunct
        self._transform_block_components(obj, obj, bigM, arg_list, suffix_list)

        # deactivate disjunct to keep the writers happy
        obj._deactivate_without_fixing_indicator()

    def _transform_block_components(self, block, disjunct, bigM, arg_list,
                                    suffix_list):
        # We first need to find any transformed disjunctions that might be here
        # because we need to move their transformation blocks up onto the parent
        # block before we transform anything else on this block 
        destinationBlock = disjunct._transformation_block().parent_block()
        for obj in block.component_data_objects(
                Disjunction, 
                sort=SortComponents.deterministic, 
                descend_into=(Block)):
            if obj.algebraic_constraint is None:
                # This could be bad if it's active since that means its
                # untransformed, but we'll wait to yell until the next loop
                continue
            # get this disjunction's relaxation block.
            transBlock = obj.algebraic_constraint().parent_block()
            
            # move transBlock up to parent component
            self._transfer_transBlock_data(transBlock, destinationBlock)
            # we leave the transformation block because it still has the XOR
            # constraints, which we want to be on the parent disjunct.

        # Now look through the component map of block and transform everything
        # we have a handler for. Yell if we don't know how to handle it. (Note
        # that because we only iterate through active components, this means
        # non-ActiveComponent types cannot have handlers.)
        for obj in block.component_objects(active=True, descend_into=False):
            handler = self.handlers.get(obj.ctype, None)
            if not handler:
                if handler is None:
                    raise GDP_Error(
                        "No BigM transformation handler registered "
                        "for modeling components of type %s. If your " 
                        "disjuncts contain non-GDP Pyomo components that "
                        "require transformation, please transform them first."
                        % obj.ctype)
                continue
            # obj is what we are transforming, we pass disjunct
            # through so that we will have access to the indicator
            # variables down the line.
            handler(obj, disjunct, bigM, arg_list, suffix_list)

    def _transfer_transBlock_data(self, fromBlock, toBlock):
        # We know that we have a list of transformed disjuncts on both. We need
        # to move those over. We know the XOR constraints are on the block, and
        # we need to leave those on the disjunct.
        disjunctList = toBlock.relaxedDisjuncts
        for idx, disjunctBlock in iteritems(fromBlock.relaxedDisjuncts):
            newblock = disjunctList[len(disjunctList)]
            newblock.transfer_attributes_from(disjunctBlock)

            # update the mappings
            original = disjunctBlock._srcDisjunct()
            original._transformation_block = weakref_ref(newblock)
            newblock._srcDisjunct = weakref_ref(original)

        # we delete this container because we just moved everything out
        del fromBlock.relaxedDisjuncts

        # Note that we could handle other components here if we ever needed
        # to, but we control what is on the transformation block and
        # currently everything is on the blocks that we just moved...

    def _warn_for_active_disjunction(self, disjunction, disjunct, bigMargs,
                                     arg_list, suffix_list):
        # this should only have gotten called if the disjunction is active
        assert disjunction.active
        problemdisj = disjunction
        if disjunction.is_indexed():
            for i in sorted(iterkeys(disjunction)):
                if disjunction[i].active:
                    # a _DisjunctionData is active, we will yell about
                    # it specifically.
                    problemdisj = disjunction[i]
                    break

        parentblock = problemdisj.parent_block()
        # the disjunction should only have been active if it wasn't transformed
        assert problemdisj.algebraic_constraint is None
        _probDisjName = problemdisj.getname(
            fully_qualified=True, name_buffer=NAME_BUFFER)
        raise GDP_Error("Found untransformed disjunction %s in disjunct %s! "
                        "The disjunction must be transformed before the "
                        "disjunct. If you are using targets, put the "
                        "disjunction before the disjunct in the list."
                        % (_probDisjName, disjunct.name))

    def _warn_for_active_disjunct(self, innerdisjunct, outerdisjunct, bigMargs,
                                  arg_list, suffix_list):
        assert innerdisjunct.active
        problemdisj = innerdisjunct
        if innerdisjunct.is_indexed():
            for i in sorted(iterkeys(innerdisjunct)):
                if innerdisjunct[i].active:
                    # This is shouldn't be true, we will complain about it.
                    problemdisj = innerdisjunct[i]
                    break

        raise GDP_Error("Found active disjunct {0} in disjunct {1}! "
                        "Either {0} "
                        "is not in a disjunction or the disjunction it is in "
                        "has not been transformed. "
                        "{0} needs to be deactivated "
                        "or its disjunction transformed before {1} can be "
                        "transformed.".format(problemdisj.name,
                                              outerdisjunct.name))

    def _transform_block_on_disjunct(self, block, disjunct, bigMargs, arg_list,
                                     suffix_list):
        # We look through everything on the component map of the block
        # and transform it just as we would if it was on the disjunct
        # directly.  (We are passing the disjunct through so that when
        # we find constraints, _xform_constraint will have access to
        # the correct indicator variable.)
        for i in sorted(iterkeys(block)):
            self._transform_block_components( block[i], disjunct, bigMargs,
                                              arg_list, suffix_list)

    def _get_constraint_map_dict(self, transBlock):
        if not hasattr(transBlock, "_constraintMap"):
            transBlock._constraintMap = {
                'srcConstraints': ComponentMap(),
                'transformedConstraints': ComponentMap()}
        return transBlock._constraintMap

    def _transform_constraint(self, obj, disjunct, bigMargs, arg_list,
                              suffix_list):
        # add constraint to the transformation block, we'll transform it there.
        transBlock = disjunct._transformation_block()
        bigm_src = transBlock.bigm_src
        constraintMap = self._get_constraint_map_dict(transBlock)
        
        disjunctionRelaxationBlock = transBlock.parent_block()
        # Though rare, it is possible to get naming conflicts here
        # since constraints from all blocks are getting moved onto the
        # same block. So we get a unique name
        cons_name = obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER)
        name = unique_component_name(transBlock, cons_name)

        if obj.is_indexed():
            try:
                newConstraint = Constraint(obj.index_set(),
                                           disjunctionRelaxationBlock.lbub)
            # HACK: We get burned by #191 here... When #1319 is merged we
            # can revist this and I think stop catching the AttributeError.
            except (TypeError, AttributeError):
                # The original constraint may have been indexed by a
                # non-concrete set (like an Any).  We will give up on
                # strict index verification and just blindly proceed.
                newConstraint = Constraint(Any)
        else:
            newConstraint = Constraint(disjunctionRelaxationBlock.lbub)
        transBlock.add_component(name, newConstraint)
        # add mapping of original constraint to transformed constraint
        constraintMap['srcConstraints'][newConstraint] = obj
        constraintMap['transformedConstraints'][obj] = newConstraint

        for i in sorted(iterkeys(obj)):
            c = obj[i]
            if not c.active:
                continue

            # first, we see if an M value was specified in the arguments.
            # (This returns None if not)
            M = self._get_M_from_args(c, bigMargs, arg_list, bigm_src)

            if __debug__ and logger.isEnabledFor(logging.DEBUG):
                _name = obj.getname(
                    fully_qualified=True, name_buffer=NAME_BUFFER)
                logger.debug("GDP(BigM): The value for M for constraint %s "
                             "from the BigM argument is %s." % (cons_name,
                                                                str(M)))

            # if we didn't get something from args, try suffixes:
            if M is None:
                M = self._get_M_from_suffixes(c, suffix_list, bigm_src)

            if __debug__ and logger.isEnabledFor(logging.DEBUG):
                _name = obj.getname(
                    fully_qualified=True, name_buffer=NAME_BUFFER)
                logger.debug("GDP(BigM): The value for M for constraint %s "
                             "after checking suffixes is %s." % (cons_name,
                                                                 str(M)))

            if not isinstance(M, (tuple, list)):
                if M is None:
                    M = (None, None)
                else:
                    try:
                        M = (-M, M)
                    except:
                        logger.error("Error converting scalar M-value %s "
                                     "to (-M,M).  Is %s not a numeric type?"
                                     % (M, type(M)))
                        raise
            if len(M) != 2:
                raise GDP_Error("Big-M %s for constraint %s is not of "
                                "length two. "
                                "Expected either a single value or "
                                "tuple or list of length two for M."
                                % (str(M), name))

            if c.lower is not None and M[0] is None:
                M = (self._estimate_M(c.body, name)[0] - c.lower, M[1])
                bigm_src[c] = M
            if c.upper is not None and M[1] is None:
                M = (M[0], self._estimate_M(c.body, name)[1] - c.upper)
                bigm_src[c] = M

            if __debug__ and logger.isEnabledFor(logging.DEBUG):
                _name = obj.getname(
                    fully_qualified=True, name_buffer=NAME_BUFFER)
                logger.debug("GDP(BigM): The value for M for constraint %s "
                             "after estimating (if needed) is %s." %
                             (cons_name, str(M)))

            # Handle indices for both SimpleConstraint and IndexedConstraint
            if i.__class__ is tuple:
                i_lb = i + ('lb',)
                i_ub = i + ('ub',)
            elif obj.is_indexed():
                i_lb = (i, 'lb',)
                i_ub = (i, 'ub',)
            else:
                i_lb = 'lb'
                i_ub = 'ub'

            if c.lower is not None:
                if M[0] is None:
                    raise GDP_Error("Cannot relax disjunctive constraint %s "
                                    "because M is not defined." % name)
                M_expr = M[0] * (1 - disjunct.indicator_var)
                newConstraint.add(i_lb, c.lower <= c. body - M_expr)
            if c.upper is not None:
                if M[1] is None:
                    raise GDP_Error("Cannot relax disjunctive constraint %s "
                                    "because M is not defined." % name)
                M_expr = M[1] * (1 - disjunct.indicator_var)
                newConstraint.add(i_ub, c.body - M_expr <= c.upper)
            # deactivate because we relaxed
            c.deactivate()

    def _get_M_from_args(self, constraint, bigMargs, arg_list, bigm_src):
        # check args: we first look in the keys for constraint and
        # constraintdata. In the absence of those, we traverse up the blocks,
        # and as a last resort check for a value for None
        if bigMargs is None:
            return None

        # check for the constraint itself and its container
        parent = constraint.parent_component()
        if constraint in bigMargs:
            m = bigMargs[constraint]
            self.used_args[constraint] = m
            bigm_src[constraint] = (bigMargs, constraint)
            return m
        elif parent in bigMargs:
            m = bigMargs[parent]
            self.used_args[parent] = m
            bigm_src[constraint] = (bigMargs, parent)
            return m

        # use the precomputed traversal up the blocks
        for arg in arg_list:
            for block, val in iteritems(arg):
                self.used_args[block] = val
                bigm_src[constraint] = (bigMargs, block)
                return val
                
        # last check for value for None!
        if None in bigMargs:
            m = bigMargs[None]
            self.used_args[None] = m
            bigm_src[constraint] = (bigMargs, None)
            return m
        return None

    def _get_M_from_suffixes(self, constraint, suffix_list, bigm_src):
        M = None
        # first we check if the constraint or its parent is a key in any of the
        # suffix lists
        for bigm in suffix_list:
            if constraint in bigm:
                M = bigm[constraint]
                bigm_src[constraint] = (bigm, constraint)
                break

            # if c is indexed, check for the parent component
            if constraint.parent_component() in bigm:
                M = bigm[constraint.parent_component()]
                bigm_src[constraint] = (bigm, constraint.parent_component())
                break

        # if we didn't get an M that way, traverse upwards through the blocks
        # and see if None has a value on any of them.
        if M is None:
            for bigm in suffix_list:
                if None in bigm:
                    M = bigm[None]
                    bigm_src[constraint] = (bigm, None)
                    break
        return M

    def _estimate_M(self, expr, name):
        # Calculate a best guess at M
        repn = generate_standard_repn(expr, quadratic=False)
        M = [0, 0]

        if not repn.is_nonlinear():
            if repn.constant is not None:
                for i in (0, 1):
                    if M[i] is not None:
                        M[i] += repn.constant

            for i, coef in enumerate(repn.linear_coefs or []):
                var = repn.linear_vars[i]
                bounds = (value(var.lb), value(var.ub))
                for i in (0, 1):
                    # reverse the bounds if the coefficient is negative
                    if coef > 0:
                        j = i
                    else:
                        j = 1 - i

                    if bounds[i] is not None:
                        M[j] += value(bounds[i]) * coef
                    else:
                        raise GDP_Error(
                            "Cannot estimate M for "
                            "expressions with unbounded variables."
                            "\n\t(found unbounded var %s while processing "
                            "constraint %s)" % (var.name, name))
        else:
            # expression is nonlinear. Try using `contrib.fbbt` to estimate.
            expr_lb, expr_ub = compute_bounds_on_expr(expr)
            if expr_lb is None or expr_ub is None:
                raise GDP_Error("Cannot estimate M for unbounded nonlinear "
                                "expressions.\n\t(found while processing "
                                "constraint %s)" % name)
            else:
                M = (expr_lb, expr_ub)

        return tuple(M)

    # These are all functions to retrieve transformed components from original
    # ones and vice versa.
    def get_src_disjunct(self, transBlock):
        """Return the Disjunct object whose transformed components are on
        transBlock.

        Parameters
        ----------
        transBlock: _BlockData which is in the relaxedDisjuncts IndexedBlock
                    on a transformation block.
        """
        try:
            return transBlock._srcDisjunct()
        except:
            raise GDP_Error("Block %s doesn't appear to be a transformation "
                            "block for a disjunct. No source disjunct found." 
                            "\n\t(original error: %s)" 
                            % (transBlock.name, sys.exc_info()[1]))

    def get_src_constraint(self, transformedConstraint):
        """Return the original Constraint whose transformed counterpart is
        transformedConstraint

        Parameters
        ----------
        transformedConstraint: Constraint, which must be a component on one of 
        the BlockDatas in the relaxedDisjuncts Block of 
        a transformation block
        """
        transBlock = transformedConstraint.parent_block()
        # This should be our block, so if it's not, the user messed up and gave
        # us the wrong thing. If they happen to also have a _constraintMap then
        # the world is really against us.
        if not hasattr(transBlock, "_constraintMap"):
            raise GDP_Error("Constraint %s is not a transformed constraint" 
                            % transformedConstraint.name)
        # if something goes wrong here, it's a bug in the mappings.
        return transBlock._constraintMap['srcConstraints'][transformedConstraint]

    def _find_parent_disjunct(self, constraint):
        # traverse up until we find the disjunct this constraint lives on
        parent_disjunct = constraint.parent_block()
        while not isinstance(parent_disjunct, _DisjunctData):
            if parent_disjunct is None:
                raise GDP_Error(
                    "Constraint %s is not on a disjunct and so was not "
                    "transformed" % constraint.name)
            parent_disjunct = parent_disjunct.parent_block()

        return parent_disjunct

    def _get_constraint_transBlock(self, constraint):
        parent_disjunct = self._find_parent_disjunct(constraint)
        # we know from _find_parent_disjunct that parent_disjunct is a Disjunct,
        # so the below is OK
        transBlock = parent_disjunct._transformation_block
        if transBlock is None:
            raise GDP_Error("Constraint %s is on a disjunct which has not been "
                            "transformed" % constraint.name)
        # if it's not None, it's the weakref we wanted.
        transBlock = transBlock()

        return transBlock

    def get_transformed_constraint(self, srcConstraint):
        """Return the transformed version of srcConstraint

        Parameters
        ----------
        srcConstraint: Constraint, which must be in the subtree of a
                       transformed Disjunct
        """
        transBlock = self._get_constraint_transBlock(srcConstraint)
        
        if hasattr(transBlock, "_constraintMap") and transBlock._constraintMap[
                'transformedConstraints'].get(srcConstraint):
            return transBlock._constraintMap['transformedConstraints'][
                srcConstraint]
        raise GDP_Error("Constraint %s has not been transformed." 
                        % srcConstraint.name)

    def get_src_disjunction(self, xor_constraint):
        """Return the Disjunction corresponding to xor_constraint

        Parameters
        ----------
        xor_constraint: Constraint, which must be the logical constraint 
                        (located on the transformation block) of some 
                        Disjunction
        """
        # NOTE: This is indeed a linear search through the Disjunctions on the
        # model. I am leaving it this way on the assumption that asking XOR
        # constraints for their Disjunction is not going to be a common
        # question. If we ever need efficiency then we should store a reverse
        # map from the XOR constraint to the Disjunction on the transformation
        # block while we do the transformation. And then this method could query
        # that map.
        m = xor_constraint.model()
        for disjunction in m.component_data_objects(Disjunction):
            if disjunction._algebraic_constraint:
                if disjunction._algebraic_constraint() is xor_constraint:
                    return disjunction
        raise GDP_Error("It appears that %s is not an XOR or OR constraint "
                        "resulting from transforming a Disjunction."
                        % xor_constraint.name)

    def get_m_value_src(self, constraint):
        """Return a tuple indicating how the M value used to transform 
        constraint was specified. (In particular, this can be used to 
        verify which BigM Suffixes were actually necessary to the 
        transformation.)

        If the M value came from an arg, returns (bigm_arg_dict, key), where 
        bigm_arg_dict is the dictionary itself and key is the key in that 
        dictionary which gave us the M value.

        If the M value came from a Suffix, returns (suffix, key) where suffix 
        is the BigM suffix used and key is the key in that Suffix.

        If the transformation calculated the value, returns (M_lower, M_upper),
        where M_lower is the float we calculated for the lower bound constraint
        and M_upper is the value calculated for the upper bound constraint.

        Parameters
        ----------
        constraint: Constraint, which must be in the subtree of a transformed 
                    Disjunct
        """
        transBlock = self._get_constraint_transBlock(constraint)
        # This is a KeyError if it fails, but it is also my fault if it
        # fails... (That is, it's a bug in the mapping.)
        return transBlock.bigm_src[constraint]
Esempio n. 12
0
def _make_heater_config_block(config):
    """
    Declare configuration options for HeaterData block.
    """
    config.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    config.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    config.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    config.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Phase equilibrium construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed, **default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}"""))
    config.declare(
        "has_pressure_change",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
constructed,
**default** - False.
**Valid values:** {
**True** - include pressure change terms,
**False** - exclude pressure change 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.}"""))
Esempio n. 13
0
class PressureChangerData(UnitModelBlockData):
    """
    Standard Compressor/Expander Unit Model Class
    """
    CONFIG = UnitModelBlockData.CONFIG()

    CONFIG.declare("material_balance_type", ConfigValue(
        default=MaterialBalanceType.useDefault,
        domain=In(MaterialBalanceType),
        description="Material balance construction flag",
        doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare("energy_balance_type", ConfigValue(
        default=EnergyBalanceType.useDefault,
        domain=In(EnergyBalanceType),
        description="Energy balance construction flag",
        doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare("momentum_balance_type", ConfigValue(
        default=MomentumBalanceType.pressureTotal,
        domain=In(MomentumBalanceType),
        description="Momentum balance construction flag",
        doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare("has_phase_equilibrium", ConfigValue(
     default=False,
     domain=In([True, False]),
     description="Phase equilibrium construction flag",
     doc="""Indicates whether terms for phase equilibrium should be
constructed, **default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}"""))
    CONFIG.declare("compressor", ConfigValue(
        default=True,
        domain=In([True, False]),
        description="Compressor flag",
        doc="""Indicates whether this unit should be considered a
            compressor (True (default), pressure increase) or an expander
            (False, pressure decrease)."""))
    CONFIG.declare("thermodynamic_assumption", ConfigValue(
        default=ThermodynamicAssumption.isothermal,
        domain=In(ThermodynamicAssumption),
        description="Thermodynamic assumption to use",
        doc="""Flag to set the thermodynamic assumption to use for the unit.
                - ThermodynamicAssumption.isothermal (default)
                - ThermodynamicAssumption.isentropic
                - ThermodynamicAssumption.pump
                - ThermodynamicAssumption.adiabatic"""))
    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.}"""))

    def build(self):
        """

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build
        super(PressureChangerData, self).build()

        # Add a control volume to the unit including setting up dynamics.
        self.control_volume = ControlVolume0DBlock(default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args})

        # Add geomerty variables to control volume
        if self.config.has_holdup:
            self.control_volume.add_geometry()

        # Add inlet and outlet state blocks to control volume
        self.control_volume.add_state_blocks(
                has_phase_equilibrium=self.config.has_phase_equilibrium)

        # Add mass balance
        # Set has_equilibrium is False for now
        # TO DO; set has_equilibrium to True
        self.control_volume.add_material_balances(
                    balance_type=self.config.material_balance_type,
                    has_phase_equilibrium=self.config.has_phase_equilibrium)

        # Add energy balance
        self.control_volume.add_energy_balances(
                    balance_type=self.config.energy_balance_type,
                    has_work_transfer=True)

        # add momentum balance
        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=True)

        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()

        # Set Unit Geometry and holdup Volume
        if self.config.has_holdup is True:
            add_object_reference(self, "volume", self.control_volume.volume)

        # Construct performance equations
        # Set references to balance terms at unit level
        # Add Work transfer variable 'work' as necessary
        add_object_reference(self, "work_mechanical", self.control_volume.work)

        # Add Momentum balance variable 'deltaP' as necessary
        add_object_reference(self, "deltaP", self.control_volume.deltaP)

        # Set reference to scaling factor for pressure in control volume
        add_object_reference(self, "sfp",
                             self.control_volume.scaling_factor_pressure)

        # Set reference to scaling factor for energy in control volume
        add_object_reference(self, "sfe",
                             self.control_volume.scaling_factor_energy)

        # Performance Variables
        self.ratioP = Var(self.flowsheet().config.time, initialize=1.0,
                          doc="Pressure Ratio")

        # Pressure Ratio
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pressure ratio constraint")
        def ratioP_calculation(b, t):
            return (self.sfp*b.ratioP[t] *
                    b.control_volume.properties_in[t].pressure ==
                    self.sfp*b.control_volume.properties_out[t].pressure)

        # Construct equations for thermodynamic assumption
        if self.config.thermodynamic_assumption == \
                ThermodynamicAssumption.isothermal:
            self.add_isothermal()
        elif self.config.thermodynamic_assumption == \
                ThermodynamicAssumption.isentropic:
            self.add_isentropic()
        elif self.config.thermodynamic_assumption == \
                ThermodynamicAssumption.pump:
            self.add_pump()
        elif self.config.thermodynamic_assumption == \
                ThermodynamicAssumption.adiabatic:
            self.add_adiabatic()

    def add_pump(self):
        """
        Add constraints for the incompressible fluid assumption

        Args:
            None

        Returns:
            None
        """

        self.work_fluid = Var(
                self.flowsheet().config.time,
                initialize=1.0,
                doc="Work required to increase the pressure of the liquid")
        self.efficiency_pump = Var(
                self.flowsheet().config.time,
                initialize=1.0,
                doc="Pump efficiency")

        @self.Constraint(self.flowsheet().config.time,
                         doc="Pump fluid work constraint")
        def fluid_work_calculation(b, t):
            return b.work_fluid[t] == (
                    (b.control_volume.properties_out[t].pressure -
                     b.control_volume.properties_in[t].pressure) *
                    b.control_volume.properties_out[t].flow_vol)

        # Actual work
        @self.Constraint(self.flowsheet().config.time,
                         doc="Actual mechanical work calculation")
        def actual_work(b, t):
            if b.config.compressor:
                return b.sfe*b.work_fluid[t] == b.sfe*(
                            b.work_mechanical[t]*b.efficiency_pump[t])
            else:
                return b.sfe*b.work_mechanical[t] == b.sfe*(
                            b.work_fluid[t]*b.efficiency_pump[t])

    def add_isothermal(self):
        """
        Add constraints for isothermal assumption.

        Args:
            None

        Returns:
            None
        """
        # Isothermal constraint
        @self.Constraint(self.flowsheet().config.time,
                         doc="For isothermal condition: Equate inlet and "
                         "outlet temperature")
        def isothermal(b, t):
            return b.control_volume.properties_in[t].temperature == \
                       b.control_volume.properties_out[t].temperature

    def add_adiabatic(self):
        """
        Add constraints for adiabatic assumption.

        Args:
            None

        Returns:
            None
        """
        # Isothermal constraint
        @self.Constraint(self.flowsheet().config.time,
                         doc="For isothermal condition: Equate inlet and "
                         "outlet enthalpy")
        def adiabatic(b, t):
            return b.control_volume.properties_in[t].enth_mol == \
                       b.control_volume.properties_out[t].enth_mol

    def add_isentropic(self):
        """
        Add constraints for isentropic assumption.

        Args:
            None

        Returns:
            None
        """
        # Get indexing sets from control volume
        # Add isentropic variables
        self.efficiency_isentropic = Var(self.flowsheet().config.time,
                                         initialize=0.8,
                                         doc="Efficiency with respect to an "
                                         "isentropic process [-]")
        self.work_isentropic = Var(self.flowsheet().config.time,
                                   initialize=0.0,
                                   doc="Work input to unit if isentropic "
                                   "process [-]")

        # Build isentropic state block
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium
        tmp_dict["parameters"] = self.config.property_package
        tmp_dict["defined_state"] = False

        self.properties_isentropic = (
                    self.config.property_package.state_block_class(
                            self.flowsheet().config.time,
                            doc="isentropic properties at outlet",
                            default=tmp_dict))

        # Connect isentropic state block properties
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pressure for isentropic calculations")
        def isentropic_pressure(b, t):
            return b.sfp*b.properties_isentropic[t].pressure == \
                b.sfp*b.control_volume.properties_out[t].pressure

        # This assumes isentropic composition is the same as outlet
        mb_type = self.config.material_balance_type
        if mb_type == MaterialBalanceType.useDefault:
            mb_type = \
                self.control_volume._get_representative_property_block() \
                .default_material_balance_type()

        if mb_type == \
                MaterialBalanceType.componentPhase:
            @self.Constraint(self.flowsheet().config.time,
                             self.config.property_package.phase_list,
                             self.config.property_package.component_list,
                             doc="Material flows for isentropic properties")
            def isentropic_material(b, t, p, j):
                return (
                    b.properties_isentropic[t].get_material_flow_terms(p, j) ==
                    b.control_volume.properties_out[t]
                    .get_material_flow_terms(p, j))
        elif mb_type == \
                MaterialBalanceType.componentTotal:
            @self.Constraint(self.flowsheet().config.time,
                             self.config.property_package.component_list,
                             doc="Material flows for isentropic properties")
            def isentropic_material(b, t, j):
                return (sum(
                    b.properties_isentropic[t].get_material_flow_terms(p, j)
                    for p in self.config.property_package.phase_list) ==
                    sum(b.control_volume.properties_out[t]
                        .get_material_flow_terms(p, j)
                        for p in self.config.property_package.phase_list))
        elif mb_type == \
                MaterialBalanceType.total:
            @self.Constraint(self.flowsheet().config.time,
                             doc="Material flows for isentropic properties")
            def isentropic_material(b, t, p, j):
                return (sum(sum(
                    b.properties_isentropic[t].get_material_flow_terms(p, j)
                    for j in self.config.property_package.component_list)
                    for p in self.config.property_package.phase_list) ==
                    sum(sum(b.control_volume.properties_out[t]
                        .get_material_flow_terms(p, j)
                        for j in self.config.property_package.component_list)
                        for p in self.config.property_package.phase_list))
        elif mb_type == \
                MaterialBalanceType.elementTotal:
            raise BalanceTypeNotSupportedError(
                    "{} PressureChanger does not support element balances."
                    .format(self.name))
        elif mb_type == \
                MaterialBalanceType.none:
            raise BalanceTypeNotSupportedError(
                    "{} PressureChanger does not support material_balance_type"
                    " = none."
                    .format(self.name))
        else:
            raise BurntToast(
                    "{} PressureChanger received an unexpected argument for "
                    "material_balance_type. This should never happen. Please "
                    "contact the IDAES developers with this bug."
                    .format(self.name))

        # This assumes isentropic entropy is the same as inlet
        @self.Constraint(self.flowsheet().config.time,
                         doc="Isentropic assumption")
        def isentropic(b, t):
            return b.properties_isentropic[t].entr_mol == \
                       b.control_volume.properties_in[t].entr_mol

        # Isentropic work
        @self.Constraint(self.flowsheet().config.time,
                         doc="Calculate work of isentropic process")
        def isentropic_energy_balance(b, t):
            return b.sfe*b.work_isentropic[t] == b.sfe*(
                sum(b.properties_isentropic[t].get_enthalpy_flow_terms(p)
                    for p in b.config.property_package.phase_list) -
                sum(b.control_volume.properties_in[t]
                    .get_enthalpy_flow_terms(p)
                    for p in b.config.property_package.phase_list))

        # Actual work
        @self.Constraint(self.flowsheet().config.time,
                         doc="Actual mechanical work calculation")
        def actual_work(b, t):
            if b.config.compressor:
                return b.sfe*b.work_isentropic[t] == b.sfe*(
                            b.work_mechanical[t]*b.efficiency_isentropic[t])
            else:
                return b.sfe*b.work_mechanical[t] == b.sfe*(
                        b.work_isentropic[t]*b.efficiency_isentropic[t])

    def model_check(blk):
        """
        Check that pressure change matches with compressor argument (i.e. if
        compressor = True, pressure should increase or work should be positive)

        Args:
            None

        Returns:
            None
        """
        if blk.config.compressor:
            # Compressor
            # Check that pressure does not decrease
            if any(blk.deltaP[t].fixed and
                    (value(blk.deltaP[t]) < 0.0)
                    for t in blk.flowsheet().config.time):
                logger.warning('{} Compressor set with negative deltaP.'
                               .format(blk.name))
            if any(blk.ratioP[t].fixed and
                    (value(blk.ratioP[t]) < 1.0)
                    for t in blk.flowsheet().config.time):
                logger.warning('{} Compressor set with ratioP less than 1.'
                               .format(blk.name))
            if any(blk.control_volume.properties_out[t].pressure.fixed and
                    (value(blk.control_volume.properties_in[t].pressure) >
                     value(blk.control_volume.properties_out[t].pressure))
                    for t in blk.flowsheet().config.time):
                logger.warning('{} Compressor set with pressure decrease.'
                               .format(blk.name))
            # Check that work is not negative
            if any(blk.work_mechanical[t].fixed and
                   (value(blk.work_mechanical[t]) < 0.0)
                   for t in blk.flowsheet().config.time):
                logger.warning('{} Compressor maybe set with negative work.'
                               .format(blk.name))
        else:
            # Expander
            # Check that pressure does not increase
            if any(blk.deltaP[t].fixed and
                    (value(blk.deltaP[t]) > 0.0)
                    for t in blk.flowsheet().config.time):
                logger.warning('{} Expander/turbine set with positive deltaP.'
                               .format(blk.name))
            if any(blk.ratioP[t].fixed and
                    (value(blk.ratioP[t]) > 1.0)
                    for t in blk.flowsheet().config.time):
                logger.warning('{} Expander/turbine set with ratioP greater '
                               'than 1.'.format(blk.name))
            if any(blk.control_volume.properties_out[t].pressure.fixed and
                    (value(blk.control_volume.properties_in[t].pressure) <
                     value(blk.control_volume.properties_out[t].pressure))
                    for t in blk.flowsheet().config.time):
                logger.warning('{} Expander/turbine maybe set with pressure ',
                               'increase.'.format(blk.name))
            # Check that work is not positive
            if any(blk.work_mechanical[t].fixed and
                   (value(blk.work_mechanical[t]) > 0.0)
                   for t in blk.flowsheet().config.time):
                logger.warning('{} Expander/turbine set with positive work.'
                               .format(blk.name))

        # Run holdup block model checks
        blk.control_volume.model_check()

        # Run model checks on isentropic property block
        try:
            for t in blk.flowsheet().config.time:
                blk.properties_in[t].model_check()
        except AttributeError:
            pass

    def initialize(blk, state_args=None, routine=None, outlvl=0,
                   solver='ipopt', optarg={'tol': 1e-6}):
        '''
        General wrapper for pressure changer initialisation routines

        Keyword Arguments:
            routine : str stating which initialization routine to execute
                        * None - use routine matching thermodynamic_assumption
                        * 'isentropic' - use isentropic initialization routine
                        * 'isothermal' - use isothermal initialization routine
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialisation routine

                     * 0 = no output (default)
                     * 1 = return solver state for each step in routine
                     * 2 = return solver state for each step in subroutines
                     * 3 = include solver output infomation (tee=True)

            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        '''
        if routine is None:
            # Use routine for specific type of unit
            routine = blk.config.thermodynamic_assumption

        # Call initialisation routine
        if routine is ThermodynamicAssumption.isentropic:
            blk.init_isentropic(state_args=state_args,
                                outlvl=outlvl,
                                solver=solver,
                                optarg=optarg)
        else:
            # Call the general initialization routine in UnitModelBlockData
            super(PressureChangerData, blk).initialize(state_args=state_args,
                                                       outlvl=outlvl,
                                                       solver=solver,
                                                       optarg=optarg)

    def init_isentropic(blk, state_args, outlvl, solver, optarg):
        '''
        Initialisation routine for unit (default solver ipopt)

        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialisation routine

                     * 0 = no output (default)
                     * 1 = return solver state for each step in routine
                     * 2 = return solver state for each step in subroutines
                     * 3 = include solver output infomation (tee=True)

            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        '''
        # Set solver options
        if outlvl > 3:
            stee = True
        else:
            stee = False

        opt = SolverFactory(solver)
        opt.options = optarg

        # ---------------------------------------------------------------------
        # Initialize Isentropic block
        blk.control_volume.properties_in.initialize(outlvl=outlvl-1,
                                                    optarg=optarg,
                                                    solver=solver,
                                                    state_args=state_args)

        if outlvl > 0:
            logger.info('{} Initialisation Step 1 Complete.'.format(blk.name))

        # ---------------------------------------------------------------------
        # Initialize holdup block
        flags = blk.control_volume.initialize(outlvl=outlvl-1,
                                              optarg=optarg,
                                              solver=solver,
                                              state_args=state_args)

        if outlvl > 0:
            logger.info('{} Initialisation Step 2 Complete.'.format(blk.name))

        # ---------------------------------------------------------------------
        # Solve for isothermal conditions
        if isinstance(
                blk.control_volume.properties_in[
                        blk.flowsheet().config.time[1]].temperature,
                Var):
            for t in blk.flowsheet().config.time:
                blk.control_volume.properties_in[t].temperature.fix()
            blk.isentropic.deactivate()
            results = opt.solve(blk, tee=stee)
            if outlvl > 0:
                if results.solver.termination_condition == \
                        TerminationCondition.optimal:
                    logger.info('{} Initialisation Step 3 Complete.'
                                .format(blk.name))
                else:
                    logger.warning('{} Initialisation Step 3 Failed.'
                                   .format(blk.name))
            for t in blk.flowsheet().config.time:
                blk.control_volume.properties_in[t].temperature.unfix()
                blk.isentropic.activate()
        elif outlvl > 0:
            logger.info('{} Initialisation Step 3 Skipped.'.format(blk.name))

        # ---------------------------------------------------------------------
        # Solve unit
        results = opt.solve(blk, tee=stee)

        if outlvl > 0:
            if results.solver.termination_condition == \
                    TerminationCondition.optimal:
                logger.info('{} Initialisation Step 4 Complete.'
                            .format(blk.name))
            else:
                logger.warning('{} Initialisation Step 4 Failed.'
                               .format(blk.name))

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.control_volume.release_state(flags, outlvl-1)

        if outlvl > 0:
            logger.info('{} Initialisation Complete.'.format(blk.name))

    def _get_performance_contents(self, time_point=0):
        var_dict = {}
        if hasattr(self, "deltaP"):
            var_dict["Mechanical Work"] = self.work_mechanical[time_point]
        if hasattr(self, "deltaP"):
            var_dict["Pressure Change"] = self.deltaP[time_point]
        if hasattr(self, "ratioP"):
            var_dict["Pressure Ratio"] = self.deltaP[time_point]
        if hasattr(self, "efficiency_pump"):
            var_dict["Efficiency"] = self.deltaP[time_point]
        if hasattr(self, "efficiency_isentropic"):
            var_dict["Isentropic Efficiency"] = self.deltaP[time_point]

        return {"vars": var_dict}
Esempio n. 14
0
def _define_turbine_multistage_config(config):
    config.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag",
            doc=
            "Only False, in a dynamic flowsheet this is psuedo-steady-state.",
        ),
    )
    config.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag",
            doc=
            "Only False, in a dynamic flowsheet this is psuedo-steady-state.",
        ),
    )
    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(
        "num_parallel_inlet_stages",
        ConfigValue(
            default=4,
            domain=int,
            description=
            "Number of parallel inlet stages to simulate partial arc "
            "admission.  Default=4",
        ),
    )
    config.declare(
        "throttle_valve_function",
        ConfigValue(
            default=ValveFunctionType.linear,
            domain=In(ValveFunctionType),
            description=
            "Valve function type, if custom provide an expression rule",
            doc=
            """The type of valve function, if custom provide an expression rule
with the valve_function_rule argument.
**default** - ValveFunctionType.linear
**Valid values** - {
ValveFunctionType.linear,
ValveFunctionType.quick_opening,
ValveFunctionType.equal_percentage,
ValveFunctionType.custom}""",
        ),
    )
    config.declare(
        "throttle_valve_function_callback",
        ConfigValue(
            default=None,
            description="A callback to add a custom valve function to the "
            "throttle valves or None.  If a callback is provided, it should "
            "take the valve block data as an argument and add a "
            "valve_function expressions to it. Default=None",
        ),
    )
    config.declare(
        "num_hp",
        ConfigValue(
            default=2,
            domain=int,
            description=
            "Number of high pressure stages not including inlet stage",
            doc="Number of high pressure stages not including inlet stage",
        ),
    )
    config.declare(
        "num_ip",
        ConfigValue(
            default=10,
            domain=int,
            description="Number of intermediate pressure stages",
            doc="Number of intermediate pressure stages",
        ),
    )
    config.declare(
        "num_lp",
        ConfigValue(
            default=5,
            domain=int,
            description=
            "Number of low pressure stages not including outlet stage",
            doc="Number of low pressure stages not including outlet stage",
        ),
    )
    config.declare(
        "hp_split_locations",
        ConfigList(
            default=[],
            domain=int,
            description="Locations of splitters in HP section",
            doc="A list of index locations of splitters in the HP section. The "
            "indexes indicate after which stage to include splitters.  0 is "
            "between the inlet stage and the first regular HP stage.",
        ),
    )
    config.declare(
        "ip_split_locations",
        ConfigList(
            default=[],
            domain=int,
            description="Locations of splitters in IP section",
            doc="A list of index locations of splitters in the IP section. The "
            "indexes indicate after which stage to include splitters.",
        ),
    )
    config.declare(
        "lp_split_locations",
        ConfigList(
            default=[],
            domain=int,
            description="Locations of splitter in LP section",
            doc="A list of index locations of splitters in the LP section. The "
            "indexes indicate after which stage to include splitters.",
        ),
    )
    config.declare(
        "hp_disconnect",
        ConfigList(
            default=[],
            domain=int,
            description="HP Turbine stages to not connect to next with an arc.",
            doc="HP Turbine stages to not connect to next with an arc. This is "
            "usually used to insert additional units between stages on a "
            "flowsheet, such as a reheater",
        ),
    )
    config.declare(
        "ip_disconnect",
        ConfigList(
            default=[],
            domain=int,
            description="IP Turbine stages to not connect to next with an arc.",
            doc="IP Turbine stages to not connect to next with an arc. This is "
            "usually used to insert additional units between stages on a "
            "flowsheet, such as a reheater",
        ),
    )
    config.declare(
        "lp_disconnect",
        ConfigList(
            default=[],
            domain=int,
            description="LP Turbine stages to not connect to next with an arc.",
            doc="LP Turbine stages to not connect to next with an arc. This is "
            "usually used to insert additional units between stages on a "
            "flowsheet, such as a reheater",
        ),
    )
    config.declare(
        "hp_split_num_outlets",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Dict, hp split index: number of splitter outlets, if not 2",
        ),
    )
    config.declare(
        "ip_split_num_outlets",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Dict, ip split index: number of splitter outlets, if not 2",
        ),
    )
    config.declare(
        "lp_split_num_outlets",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Dict, lp split index: number of splitter outlets, if not 2",
        ),
    )
Esempio n. 15
0
class HelmTurbineMultistageData(UnitModelBlockData):
    CONFIG = ConfigBlock()
    _define_turbine_multistage_config(CONFIG)

    def build(self):
        super().build()
        config = self.config
        unit_cfg = {  # general unit model config
            "dynamic": config.dynamic,
            "has_holdup": config.has_holdup,
            "property_package": config.property_package,
            "property_package_args": config.property_package_args,
        }
        ni = self.config.num_parallel_inlet_stages
        inlet_idx = self.inlet_stage_idx = pyo.RangeSet(ni)

        thrtl_cfg = unit_cfg.copy()
        thrtl_cfg["valve_function"] = self.config.throttle_valve_function
        thrtl_cfg["valve_function_callback"] = \
            self.config.throttle_valve_function_callback

        # Adding unit models
        # ------------------------

        # Splitter to inlet that splits main flow into parallel flows for
        # paritial arc admission to the turbine
        self.inlet_split = HelmSplitter(default=self._split_cfg(unit_cfg, ni))
        self.throttle_valve = SteamValve(inlet_idx, default=thrtl_cfg)
        self.inlet_stage = HelmTurbineInletStage(inlet_idx, default=unit_cfg)
        # mixer to combine the parallel flows back together
        self.inlet_mix = HelmMixer(default=self._mix_cfg(unit_cfg, ni))
        # add turbine sections.
        # inlet stage -> hp stages -> ip stages -> lp stages -> outlet stage
        self.hp_stages = HelmTurbineStage(pyo.RangeSet(config.num_hp),
                                          default=unit_cfg)
        self.ip_stages = HelmTurbineStage(pyo.RangeSet(config.num_ip),
                                          default=unit_cfg)
        self.lp_stages = HelmTurbineStage(pyo.RangeSet(config.num_lp),
                                          default=unit_cfg)
        self.outlet_stage = HelmTurbineOutletStage(default=unit_cfg)

        for i in self.hp_stages:
            self.hp_stages[i].ratioP.fix()
            self.hp_stages[i].efficiency_isentropic.fix()
        for i in self.ip_stages:
            self.ip_stages[i].ratioP.fix()
            self.ip_stages[i].efficiency_isentropic.fix()
        for i in self.lp_stages:
            self.lp_stages[i].ratioP.fix()
            self.lp_stages[i].efficiency_isentropic.fix()

        # Then make splitter config.  If number of outlets is specified
        # make a specific config, otherwise use default with 2 outlets
        s_sfg_default = self._split_cfg(unit_cfg, 2)
        hp_splt_cfg = {}
        ip_splt_cfg = {}
        lp_splt_cfg = {}
        # Now to finish up if there are more than two outlets, set that
        for i, v in config.hp_split_num_outlets.items():
            hp_splt_cfg[i] = self._split_cfg(unit_cfg, v)
        for i, v in config.ip_split_num_outlets.items():
            ip_splt_cfg[i] = self._split_cfg(unit_cfg, v)
        for i, v in config.lp_split_num_outlets.items():
            lp_splt_cfg[i] = self._split_cfg(unit_cfg, v)
        # put in splitters for turbine steam extractions
        if config.hp_split_locations:
            self.hp_split = HelmSplitter(config.hp_split_locations,
                                         default=s_sfg_default,
                                         initialize=hp_splt_cfg)
        else:
            self.hp_split = {}
        if config.ip_split_locations:
            self.ip_split = HelmSplitter(config.ip_split_locations,
                                         default=s_sfg_default,
                                         initialize=ip_splt_cfg)
        else:
            self.ip_split = {}
        if config.lp_split_locations:
            self.lp_split = HelmSplitter(config.lp_split_locations,
                                         default=s_sfg_default,
                                         initialize=lp_splt_cfg)
        else:
            self.lp_split = {}
        # Done with unit models.  Adding Arcs (streams).
        # ------------------------------------------------

        # First up add streams in the inlet section
        def _split_to_rule(b, i):
            return {
                "source": getattr(self.inlet_split, "outlet_{}".format(i)),
                "destination": self.throttle_valve[i].inlet,
            }

        def _valve_to_rule(b, i):
            return {
                "source": self.throttle_valve[i].outlet,
                "destination": self.inlet_stage[i].inlet,
            }

        def _inlet_to_rule(b, i):
            return {
                "source": self.inlet_stage[i].outlet,
                "destination": getattr(self.inlet_mix, "inlet_{}".format(i)),
            }

        self.stream_throttle_inlet = Arc(inlet_idx, rule=_split_to_rule)
        self.stream_throttle_outlet = Arc(inlet_idx, rule=_valve_to_rule)
        self.stream_inlet_mix_inlet = Arc(inlet_idx, rule=_inlet_to_rule)

        # There are three sections HP, IP, and LP which all have the same sort
        # of internal connctions, so the functions below provide some generic
        # capcbilities for adding the internal Arcs (streams).
        def _arc_indexes(nstages, index_set, discon, splits):
            """
            This takes the index set of all possible streams in a turbine
            section and throws out arc indexes for stages that are disconnected
            and arc indexes that are not needed because there is no splitter
            after a stage.

            Args:
                nstages (int): Number of stages in section
                index_set (Set): Index set for arcs in the section
                discon (list): Disconnected stages in the section
                splits (list): Spliter locations
            """
            sr = set()  # set of things to remove from the Arc index set
            for i in index_set:
                if (i[0] in discon or i[0] == nstages) and i[0] in splits:
                    # don't connect stage i to next remove stream after split
                    sr.add((i[0], 2))
                elif (i[0] in discon
                      or i[0] == nstages) and i[0] not in splits:
                    # no splitter and disconnect so remove both streams
                    sr.add((i[0], 1))
                    sr.add((i[0], 2))
                elif i[0] not in splits:
                    # no splitter and not disconnected so just second stream
                    sr.add((i[0], 2))
                else:
                    # has splitter so need both streams don't remove anything
                    pass
            for i in sr:  # remove the unneeded Arc indexes
                index_set.remove(i)

        def _arc_rule(turbines, splitters):
            """
            This creates a rule function for arcs in a turbine section. When
            this is used, the indexes for nonexistant stream will have already
            been removed, so any indexes the rule will get should have a stream
            associated.

            Args:
                turbines (TurbineStage): Indexed block with turbine section stages
                splitters (Separator): Indexed block of splitters
            """
            def _rule(b, i, j):
                if i in splitters and j == 1:  # stage to splitter
                    return {
                        "source": turbines[i].outlet,
                        "destination": splitters[i].inlet,
                    }
                elif j == 2:  # splitter to next stage
                    return {
                        "source": splitters[i].outlet_1,
                        "destination": turbines[i + 1].inlet,
                    }
                else:  # no splitter, stage to next stage
                    return {
                        "source": turbines[i].outlet,
                        "destination": turbines[i + 1].inlet,
                    }

            return _rule

        # Create initial arcs index sets with all possible streams
        self.hp_stream_idx = pyo.Set(initialize=self.hp_stages.index_set() *
                                     [1, 2])
        self.ip_stream_idx = pyo.Set(initialize=self.ip_stages.index_set() *
                                     [1, 2])
        self.lp_stream_idx = pyo.Set(initialize=self.lp_stages.index_set() *
                                     [1, 2])

        # Throw out unneeded streams for disconnected stages or no splitter
        _arc_indexes(
            config.num_hp,
            self.hp_stream_idx,
            config.hp_disconnect,
            config.hp_split_locations,
        )
        _arc_indexes(
            config.num_ip,
            self.ip_stream_idx,
            config.ip_disconnect,
            config.ip_split_locations,
        )
        _arc_indexes(
            config.num_lp,
            self.lp_stream_idx,
            config.lp_disconnect,
            config.lp_split_locations,
        )

        # Create connections internal to each turbine section (hp, ip, and lp)
        self.hp_stream = Arc(self.hp_stream_idx,
                             rule=_arc_rule(self.hp_stages, self.hp_split))
        self.ip_stream = Arc(self.ip_stream_idx,
                             rule=_arc_rule(self.ip_stages, self.ip_split))
        self.lp_stream = Arc(self.lp_stream_idx,
                             rule=_arc_rule(self.lp_stages, self.lp_split))

        # Connect hp section to ip section unless its a disconnect location
        last_hp = config.num_hp
        if 0 not in config.ip_disconnect and last_hp not in config.hp_disconnect:
            # Not disconnected stage so add stream, depending on splitter existance
            if last_hp in config.hp_split_locations:  # connect splitter to ip
                self.hp_to_ip_stream = Arc(
                    source=self.hp_split[last_hp].outlet_1,
                    destination=self.ip_stages[1].inlet,
                )
            else:  # connect last hp to ip
                self.hp_to_ip_stream = Arc(
                    source=self.hp_stages[last_hp].outlet,
                    destination=self.ip_stages[1].inlet,
                )
        # Connect ip section to lp section unless its a disconnect location
        last_ip = config.num_ip
        if 0 not in config.lp_disconnect and last_ip not in config.ip_disconnect:
            if last_ip in config.ip_split_locations:  # connect splitter to ip
                self.ip_to_lp_stream = Arc(
                    source=self.ip_split[last_ip].outlet_1,
                    destination=self.lp_stages[1].inlet,
                )
            else:  # connect last hp to ip
                self.ip_to_lp_stream = Arc(
                    source=self.ip_stages[last_ip].outlet,
                    destination=self.lp_stages[1].inlet,
                )
        # Connect inlet stage to hp section
        #   not allowing disconnection of inlet and first regular hp stage
        if 0 in config.hp_split_locations:
            # connect inlet mix to splitter and splitter to hp section
            self.inlet_to_splitter_stream = Arc(
                source=self.inlet_mix.outlet,
                destination=self.hp_split[0].inlet)
            self.splitter_to_hp_stream = Arc(
                source=self.hp_split[0].outlet_1,
                destination=self.hp_stages[1].inlet)
        else:  # connect mixer to first hp turbine stage
            self.inlet_to_hp_stream = Arc(source=self.inlet_mix.outlet,
                                          destination=self.hp_stages[1].inlet)

        self.power = pyo.Var(self.flowsheet().time,
                             initialize=-1e8,
                             doc="power (W)")

        @self.Constraint(self.flowsheet().time)
        def power_eqn(b, t):
            return (b.power[t] == b.outlet_stage.control_volume.work[t] *
                    b.outlet_stage.efficiency_mech +
                    sum(b.inlet_stage[i].control_volume.work[t] *
                        b.inlet_stage[i].efficiency_mech
                        for i in b.inlet_stage) +
                    sum(b.hp_stages[i].control_volume.work[t] *
                        b.hp_stages[i].efficiency_mech for i in b.hp_stages) +
                    sum(b.ip_stages[i].control_volume.work[t] *
                        b.ip_stages[i].efficiency_mech for i in b.ip_stages) +
                    sum(b.lp_stages[i].control_volume.work[t] *
                        b.lp_stages[i].efficiency_mech for i in b.lp_stages))

        # Connect lp section to outlet stage, not allowing outlet stage to be
        # disconnected
        last_lp = config.num_lp
        if last_lp in config.lp_split_locations:  # connect splitter to outlet
            self.lp_to_outlet_stream = Arc(
                source=self.lp_split[last_lp].outlet_1,
                destination=self.outlet_stage.inlet,
            )
        else:  # connect last lpstage to outlet
            self.lp_to_outlet_stream = Arc(
                source=self.lp_stages[last_lp].outlet,
                destination=self.outlet_stage.inlet,
            )
        pyo.TransformationFactory("network.expand_arcs").apply_to(self)

    def _split_cfg(self, unit_cfg, no=2):
        """
        This creates a configuration dictionary for a splitter.

        Args:
            unit_cfg: The base unit config dict.
            no: Number of outlets, default=2
        """
        # Create a dict for splitter config args
        cfg = copy.copy(unit_cfg)
        cfg.update(num_outlets=no)
        return cfg

    def _mix_cfg(self, unit_cfg, ni=2):
        """
        This creates a configuration dictionary for a mixer.

        Args:
            unit_cfg: The base unit config dict.
            ni: Number of inlets, default=2
        """
        cfg = copy.copy(unit_cfg)
        cfg.update(
            num_inlets=ni,
            momentum_mixing_type=MomentumMixingType.minimize_and_equality)
        return cfg

    def throttle_cv_fix(self, value):
        """
        Fix the thottle valve coefficients.  These are generally the same for
        each of the parallel stages so this provides a convenient way to set
        them.

        Args:
            value: The value to fix the turbine inlet flow coefficients at
        """
        for i in self.throttle_valve:
            self.throttle_valve[i].Cv.fix(value)

    def turbine_inlet_cf_fix(self, value):
        """
        Fix the inlet turbine stage flow coefficient.  These are
        generally the same for each of the parallel stages so this provides
        a convenient way to set them.

        Args:
            value: The value to fix the turbine inlet flow coefficients at
        """
        for i in self.inlet_stage:
            self.inlet_stage[i].flow_coeff.fix(value)

    def _init_section(
        self,
        stages,
        splits,
        disconnects,
        prev_port,
        outlvl,
        solver,
        optarg,
        copy_disconneted_flow,
        copy_disconneted_pressure,
    ):
        """ Reuse the initializtion for HP, IP and, LP sections.
        """
        if 0 in splits:
            copy_port(splits[0].inlet, prev_port)
            splits[0].initialize(outlvl=outlvl, solver=solver, optarg=optarg)
            prev_port = splits[0].outlet_1
        for i in stages:
            if i - 1 not in disconnects:
                copy_port(stages[i].inlet, prev_port)
            else:
                if copy_disconneted_flow:
                    for t in stages[i].inlet.flow_mol:
                        stages[i].inlet.flow_mol[t] = pyo.value(
                            prev_port.flow_mol[t])
                if copy_disconneted_pressure:
                    for t in stages[i].inlet.pressure:
                        stages[i].inlet.pressure[t] = pyo.value(
                            prev_port.pressure[t])
            stages[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg)
            prev_port = stages[i].outlet
            if i in splits:
                copy_port(splits[i].inlet, prev_port)
                splits[i].initialize(outlvl=outlvl,
                                     solver=solver,
                                     optarg=optarg)
                prev_port = splits[i].outlet_1
        return prev_port

    def turbine_outlet_cf_fix(self, value):
        """
        Fix the inlet turbine stage flow coefficient.  These are
        generally the same for each of the parallel stages so this provides
        a convenient way to set them.

        Args:
            value: The value to fix the turbine inlet flow coefficients at
        """
        self.outlet_stage.flow_coeff.fix(value)

    def initialize(self,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   flow_iterate=2,
                   optarg=None,
                   copy_disconneted_flow=True,
                   copy_disconneted_pressure=True,
                   calculate_outlet_cf=False,
                   calculate_inlet_cf=False):
        """
        Initialize

        Args:
            outlvl: logging level default is NOTSET, which inherits from the
                parent logger
            solver: the NL solver
            flow_iterate: If not calculating flow coefficients, this is the
                number of times to update the flow and repeat initialization
                (1 to 5 where 1 does not update the flow guess)
            optarg: solver arguments, default is None
            copy_disconneted_flow: Copy the flow through the disconnected stages
                default is True
            copy_disconneted_pressure: Copy the pressure through the disconnected
                stages default is True
            calculate_outlet_cf: Use the flow initial flow guess to calculate
                the outlet stage flow coefficient, default is False,
            calculate_inlet_cf: Use the inlet stage ratioP to calculate the flow
                coefficent for the inlet stage default is False

        Returns:
            None
        """
        # Setup loggers
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")
        # Store initial model specs, restored at the end of initializtion, so
        # the problem is not altered.  This can restore fixed/free vars,
        # active/inactive constraints, and fixed variable values.
        sp = StoreSpec.value_isfixed_isactive(only_fixed=True)
        istate = to_json(self, return_dict=True, wts=sp)

        # Assume the flow into the turbine is a reasonable guess for
        # initializtion
        flow_guess = self.inlet_split.inlet.flow_mol[0].value

        for it_count in range(flow_iterate):
            self.inlet_split.initialize(outlvl=outlvl,
                                        solver=solver,
                                        optarg=optarg)

            # Initialize valves
            for i in self.inlet_stage_idx:
                u = self.throttle_valve[i]
                copy_port(u.inlet,
                          getattr(self.inlet_split, "outlet_{}".format(i)))
                u.initialize(outlvl=outlvl, solver=solver, optarg=optarg)

            # Initialize turbine
            for i in self.inlet_stage_idx:
                u = self.inlet_stage[i]
                copy_port(u.inlet, self.throttle_valve[i].outlet)
                u.initialize(outlvl=outlvl,
                             solver=solver,
                             optarg=optarg,
                             calculate_cf=calculate_inlet_cf)

            # Initialize Mixer
            self.inlet_mix.use_minimum_inlet_pressure_constraint()
            for i in self.inlet_stage_idx:
                copy_port(
                    getattr(self.inlet_mix, "inlet_{}".format(i)),
                    self.inlet_stage[i].outlet,
                )
                getattr(self.inlet_mix, "inlet_{}".format(i)).fix()
            self.inlet_mix.initialize(outlvl=outlvl,
                                      solver=solver,
                                      optarg=optarg)
            for i in self.inlet_stage_idx:
                getattr(self.inlet_mix, "inlet_{}".format(i)).unfix()
            self.inlet_mix.use_equal_pressure_constraint()

            prev_port = self.inlet_mix.outlet
            prev_port = self._init_section(
                self.hp_stages,
                self.hp_split,
                self.config.hp_disconnect,
                prev_port,
                outlvl,
                solver,
                optarg,
                copy_disconneted_flow=copy_disconneted_flow,
                copy_disconneted_pressure=copy_disconneted_pressure,
            )
            if len(self.hp_stages) in self.config.hp_disconnect:
                self.config.ip_disconnect.append(0)
            prev_port = self._init_section(
                self.ip_stages,
                self.ip_split,
                self.config.ip_disconnect,
                prev_port,
                outlvl,
                solver,
                optarg,
                copy_disconneted_flow=copy_disconneted_flow,
                copy_disconneted_pressure=copy_disconneted_pressure,
            )
            if len(self.ip_stages) in self.config.ip_disconnect:
                self.config.lp_disconnect.append(0)
            prev_port = self._init_section(
                self.lp_stages,
                self.lp_split,
                self.config.lp_disconnect,
                prev_port,
                outlvl,
                solver,
                optarg,
                copy_disconneted_flow=copy_disconneted_flow,
                copy_disconneted_pressure=copy_disconneted_pressure,
            )

            copy_port(self.outlet_stage.inlet, prev_port)
            self.outlet_stage.initialize(outlvl=outlvl,
                                         solver=solver,
                                         optarg=optarg,
                                         calculate_cf=calculate_outlet_cf)
            if calculate_outlet_cf:
                break
            if it_count < flow_iterate - 1:
                for t in self.inlet_split.inlet.flow_mol:
                    self.inlet_split.inlet.flow_mol[t].value = \
                        self.outlet_stage.inlet.flow_mol[t].value

                    for s in self.hp_split.values():
                        for i, o in enumerate(s.outlet_list):
                            if i == 0:
                                continue
                            o = getattr(s, o)
                            self.inlet_split.inlet.flow_mol[t].value += \
                                o.flow_mol[t].value
                    for s in self.ip_split.values():
                        for i, o in enumerate(s.outlet_list):
                            if i == 0:
                                continue
                            o = getattr(s, o)
                            self.inlet_split.inlet.flow_mol[t].value += \
                                o.flow_mol[t].value
                    for s in self.lp_split.values():
                        for i, o in enumerate(s.outlet_list):
                            if i == 0:
                                continue
                            o = getattr(s, o)
                            self.inlet_split.inlet.flow_mol[t].value += \
                                o.flow_mol[t].value

        if calculate_inlet_cf:
            # cf was probably fixed, so will have to set the value agian here
            # if you ask for it to be calculated.
            icf = {}
            for i in self.inlet_stage:
                for t in self.inlet_stage[i].flow_coeff:
                    icf[i, t] = pyo.value(self.inlet_stage[i].flow_coeff[t])
        if calculate_outlet_cf:
            ocf = pyo.value(self.outlet_stage.flow_coeff)

        from_json(self, sd=istate, wts=sp)

        if calculate_inlet_cf:
            # cf was probably fixed, so will have to set the value agian here
            # if you ask for it to be calculated.
            for t in self.inlet_stage[i].flow_coeff:
                for i in self.inlet_stage:
                    self.inlet_stage[i].flow_coeff[t] = icf[i, t]
        if calculate_outlet_cf:
            self.outlet_stage.flow_coeff = ocf

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        # Add a default power scale
        # pretty safe to say power is around 100 to 1000 MW

        for t in self.power:
            if iscale.get_scaling_factor(self.power[t]) is None:
                iscale.set_scaling_factor(self.power[t], 1e-8)

        for t, c in self.power_eqn.items():
            power_scale = iscale.get_scaling_factor(self.power[t],
                                                    default=1,
                                                    warning=True)
            # Set power equation scale factor
            iscale.constraint_scaling_transform(c,
                                                power_scale,
                                                overwrite=False)
Esempio n. 16
0
def _process_kwargs(o, kwargs):
    kwargs.setdefault("rule", _rule_default)
    o._block_data_config_default = kwargs.pop("default", None)
    o._block_data_config_initialize = ConfigBlock(implicit=True)
    o._block_data_config_initialize.set_value(kwargs.pop("initialize", None))
    o._idx_map = kwargs.pop("idx_map", None)
Esempio n. 17
0
class PFRData(UnitModelBlockData):
    """
    Standard Plug Flow Reactor Unit Model Class
    """
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare(
        "has_equilibrium_reactions",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Equilibrium reaction construction flag",
            doc="""Indicates whether terms for equilibrium controlled reactions
should be constructed,
**default** - True.
**Valid values:** {
**True** - include equilibrium reaction terms,
**False** - exclude equilibrium reaction terms.}"""))
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Phase equilibrium construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed,
**default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}"""))
    CONFIG.declare(
        "has_heat_of_reaction",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Heat of reaction term construction flag",
            doc="""Indicates whether terms for heat of reaction terms should be
constructed,
**default** - False.
**Valid values:** {
**True** - include heat of reaction terms,
**False** - exclude heat of reaction terms.}"""))
    CONFIG.declare(
        "has_heat_transfer",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Heat transfer term construction flag",
            doc=
            """Indicates whether terms for heat transfer should be constructed,
**default** - False.
**Valid values:** {
**True** - include heat transfer terms,
**False** - exclude heat transfer terms.}"""))
    CONFIG.declare(
        "has_pressure_change",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
constructed,
**default** - False.
**Valid values:** {
**True** - include pressure change terms,
**False** - exclude pressure change 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(
        "length_domain_set",
        ConfigValue(
            default=[0.0, 1.0],
            domain=ListOf(float),
            description="List of points to use to initialize length domain",
            doc=
            """A list of values to be used when constructing the length domain
of the reactor. Point must lie between 0.0 and 1.0,
**default** - [0.0, 1.0].
**Valid values:** {
a list of floats}"""))
    CONFIG.declare(
        "transformation_method",
        ConfigValue(
            default="dae.finite_difference",
            description="Method to use for DAE transformation",
            doc="""Method to use to transform domain. Must be a method recognised
by the Pyomo TransformationFactory,
**default** - "dae.finite_difference"."""))
    CONFIG.declare(
        "transformation_scheme",
        ConfigValue(default="BACKWARD",
                    description="Scheme to use for DAE transformation",
                    doc="""Scheme to use when transformating domain. See Pyomo
documentation for supported schemes,
**default** - "BACKWARD"."""))
    CONFIG.declare(
        "finite_elements",
        ConfigValue(
            default=20,
            description=
            "Number of finite elements to use for DAE transformation",
            doc="""Number of finite elements to use when transforming length
domain,
**default** - 20."""))
    CONFIG.declare(
        "collocation_points",
        ConfigValue(
            default=3,
            description="No. collocation points to use for DAE transformation",
            doc="""Number of collocation points to use when transforming length
domain,
**default** - 3."""))

    def build(self):
        """
        Begin building model (pre-DAE transformation).

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(PFRData, self).build()

        # Build Control Volume
        self.control_volume = ControlVolume1DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
                "reaction_package": self.config.reaction_package,
                "reaction_package_args": self.config.reaction_package_args,
                "transformation_method": self.config.transformation_method,
                "transformation_scheme": self.config.transformation_scheme,
                "finite_elements": self.config.finite_elements,
                "collocation_points": self.config.collocation_points
            })

        self.control_volume.add_geometry(
            length_domain_set=self.config.length_domain_set)

        self.control_volume.add_state_blocks(
            has_phase_equilibrium=self.config.has_phase_equilibrium)

        self.control_volume.add_reaction_blocks(
            has_equilibrium=self.config.has_equilibrium_reactions)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_rate_reactions=True,
            has_equilibrium_reactions=self.config.has_equilibrium_reactions,
            has_phase_equilibrium=self.config.has_phase_equilibrium)

        self.control_volume.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_of_reaction=self.config.has_heat_of_reaction,
            has_heat_transfer=self.config.has_heat_transfer)

        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        self.control_volume.apply_transformation()

        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()

        # Add PFR performance equation
        @self.Constraint(self.flowsheet().time,
                         self.control_volume.length_domain,
                         self.config.reaction_package.rate_reaction_idx,
                         doc="PFR performance equation")
        def performance_eqn(b, t, x, r):
            return b.control_volume.rate_reaction_extent[t, x, r] == (
                b.control_volume.reactions[t, x].reaction_rate[r] *
                b.control_volume.area)

        # Set references to balance terms at unit level
        add_object_reference(self, "length", self.control_volume.length)
        add_object_reference(self, "area", self.control_volume.area)

        # Add volume variable for full reactor
        units = self.config.property_package.get_metadata()
        self.volume = Var(initialize=1,
                          doc="Reactor Volume",
                          units=units.get_derived_units("volume"))

        self.geometry = Constraint(expr=self.volume == self.area * self.length)

        if (self.config.has_heat_transfer is True
                and self.config.energy_balance_type != EnergyBalanceType.none):
            self.heat_duty = Reference(self.control_volume.heat[...])
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            self.deltaP = Reference(self.control_volume.deltaP[...])

    def _get_performance_contents(self, time_point=0):
        var_dict = {"Volume": self.volume}
        var_dict = {"Length": self.length}
        var_dict = {"Area": self.area}

        return {"vars": var_dict}

    def get_costing(self, year=None, module=costing, **kwargs):
        if not hasattr(self.flowsheet(), "costing"):
            self.flowsheet().get_costing(year=year, module=module)

        self.costing = Block()
        units_meta = (
            self.config.property_package.get_metadata().get_derived_units)
        self.diameter = Var(initialize=1,
                            units=units_meta('length'),
                            doc='vessel diameter')
        self.diameter_eq = Constraint(
            expr=self.volume == (self.length * const.pi * self.diameter**2) /
            4)
        module.pfr_costing(self.costing, **kwargs)
Esempio n. 18
0
class Collocation_Discretization_Transformation(Transformation):

    CONFIG = ConfigBlock("dae.collocation")
    CONFIG.declare(
        'nfe',
        ConfigValue(
            default=10,
            domain=PositiveInt,
            description="The desired number of finite element points to be "
            "included in the discretization"))
    CONFIG.declare(
        'ncp',
        ConfigValue(
            default=3,
            domain=PositiveInt,
            description="The desired number of collocation points over each "
            "finite element"))
    CONFIG.declare(
        'wrt',
        ConfigValue(
            default=None,
            description="The ContinuousSet to be discretized",
            doc="Indicates which ContinuousSet the transformation should be "
            "applied to. If this keyword argument is not specified then the "
            "same scheme will be applied to all ContinuousSets."))
    CONFIG.declare(
        'scheme',
        ConfigValue(
            default='LAGRANGE-RADAU',
            domain=In(['LAGRANGE-RADAU', 'LAGRANGE-LEGENDRE']),
            description="Indicates which collocation scheme to apply",
            doc="Options are 'LAGRANGE-RADAU' and 'LAGRANGE-LEGENDRE'. "
            "The default scheme is Lagrange polynomials with Radau roots"))

    def __init__(self):
        super(Collocation_Discretization_Transformation, self).__init__()
        self._ncp = {}
        self._nfe = {}
        self._adot = {}
        self._adotdot = {}
        self._afinal = {}
        self._tau = {}
        self._reduced_cp = {}
        self.all_schemes = {
            'LAGRANGE-RADAU':
            (_lagrange_radau_transform, _lagrange_radau_transform_order2),
            'LAGRANGE-LEGENDRE':
            (_lagrange_legendre_transform, _lagrange_legendre_transform_order2)
        }

    def _get_radau_constants(self, currentds):
        """
        This function sets the radau collocation points and a values depending
        on how many collocation points have been specified and whether or not
        the user has numpy
        """
        if not numpy_available:
            if self._ncp[currentds] > 10:
                raise ValueError("Numpy was not found so the maximum number "
                                 "of collocation points is 10")
            from pyomo.dae.utilities import (radau_tau_dict, radau_adot_dict,
                                             radau_adotdot_dict)
            self._tau[currentds] = radau_tau_dict[self._ncp[currentds]]
            self._adot[currentds] = radau_adot_dict[self._ncp[currentds]]
            self._adotdot[currentds] = radau_adotdot_dict[self._ncp[currentds]]
            self._afinal[currentds] = None
        else:
            alpha = 1
            beta = 0
            k = self._ncp[currentds] - 1
            cp = sorted(list(calc_cp(alpha, beta, k)))
            cp.insert(0, 0.0)
            cp.append(1.0)
            adot = calc_adot(cp, 1)
            adotdot = calc_adot(cp, 2)

            self._tau[currentds] = cp
            self._adot[currentds] = adot
            self._adotdot[currentds] = adotdot
            self._afinal[currentds] = None

    def _get_legendre_constants(self, currentds):
        """
        This function sets the legendre collocation points and a values
        depending on how many collocation points have been specified and
        whether or not the user has numpy
        """
        if not numpy_available:
            if self._ncp[currentds] > 10:
                raise ValueError("Numpy was not found so the maximum number "
                                 "of collocation points is 10")
            from pyomo.dae.utilities import (legendre_tau_dict,
                                             legendre_adot_dict,
                                             legendre_adotdot_dict,
                                             legendre_afinal_dict)
            self._tau[currentds] = legendre_tau_dict[self._ncp[currentds]]
            self._adot[currentds] = legendre_adot_dict[self._ncp[currentds]]
            self._adotdot[currentds] = \
                legendre_adotdot_dict[self._ncp[currentds]]
            self._afinal[currentds] = \
                legendre_afinal_dict[self._ncp[currentds]]
        else:
            alpha = 0
            beta = 0
            k = self._ncp[currentds]
            cp = sorted(list(calc_cp(alpha, beta, k)))
            cp.insert(0, 0.0)
            adot = calc_adot(cp, 1)
            adotdot = calc_adot(cp, 2)
            afinal = calc_afinal(cp)

            self._tau[currentds] = cp
            self._adot[currentds] = adot
            self._adotdot[currentds] = adotdot
            self._afinal[currentds] = afinal

    def _apply_to(self, instance, **kwds):
        """
        Applies specified collocation transformation to a modeling instance

        Keyword Arguments:
        nfe           The desired number of finite element points to be
                      included in the discretization.
        ncp           The desired number of collocation points over each
                      finite element.
        wrt           Indicates which ContinuousSet the transformation
                      should be applied to. If this keyword argument is not
                      specified then the same scheme will be applied to all
                      ContinuousSets.
        scheme        Indicates which collocation scheme to apply.
                      Options are 'LAGRANGE-RADAU' and 'LAGRANGE-LEGENDRE'. 
                      The default scheme is Lagrange polynomials with Radau
                      roots.
        """

        config = self.CONFIG(kwds)

        tmpnfe = config.nfe
        tmpncp = config.ncp
        tmpds = config.wrt

        if tmpds is not None:
            if tmpds.ctype is not ContinuousSet:
                raise TypeError("The component specified using the 'wrt' "
                                "keyword must be a continuous set")
            elif 'scheme' in tmpds.get_discretization_info():
                raise ValueError(
                    "The discretization scheme '%s' has already "
                    "been applied to the ContinuousSet '%s'" %
                    (tmpds.get_discretization_info()['scheme'], tmpds.name))

        if None in self._nfe:
            raise ValueError(
                "A general discretization scheme has already been applied to "
                "to every ContinuousSet in the model. If you would like to "
                "specify a specific discretization scheme for one of the "
                "ContinuousSets you must discretize each ContinuousSet "
                "separately.")

        if len(self._nfe) == 0 and tmpds is None:
            # Same discretization on all ContinuousSets
            self._nfe[None] = tmpnfe
            self._ncp[None] = tmpncp
            currentds = None
        else:
            self._nfe[tmpds.name] = tmpnfe
            self._ncp[tmpds.name] = tmpncp
            currentds = tmpds.name

        self._scheme_name = config.scheme
        self._scheme = self.all_schemes.get(self._scheme_name, None)

        if self._scheme_name == 'LAGRANGE-RADAU':
            self._get_radau_constants(currentds)
        elif self._scheme_name == 'LAGRANGE-LEGENDRE':
            self._get_legendre_constants(currentds)

        self._transformBlock(instance, currentds)

        return instance

    def _transformBlock(self, block, currentds):

        self._fe = {}
        for ds in block.component_objects(ContinuousSet, descend_into=True):
            if currentds is None or currentds == ds.name:
                if 'scheme' in ds.get_discretization_info():
                    raise DAE_Error(
                        "Attempting to discretize ContinuousSet "
                        "'%s' after it has already been discretized. " %
                        ds.name)
                generate_finite_elements(ds, self._nfe[currentds])
                if not ds.get_changed():
                    if len(ds) - 1 > self._nfe[currentds]:
                        logger.warning(
                            "More finite elements were found in "
                            "ContinuousSet '%s' than the number of "
                            "finite elements specified in apply. The "
                            "larger number of finite elements will be "
                            "used." % ds.name)

                self._nfe[ds.name] = len(ds) - 1
                self._fe[ds.name] = list(ds)
                generate_colloc_points(ds, self._tau[currentds])
                # Adding discretization information to the continuousset
                # object itself so that it can be accessed outside of the
                # discretization object
                disc_info = ds.get_discretization_info()
                disc_info['nfe'] = self._nfe[ds.name]
                disc_info['ncp'] = self._ncp[currentds]
                disc_info['tau_points'] = self._tau[currentds]
                disc_info['adot'] = self._adot[currentds]
                disc_info['adotdot'] = self._adotdot[currentds]
                disc_info['afinal'] = self._afinal[currentds]
                disc_info['scheme'] = self._scheme_name

        expand_components(block)

        for d in block.component_objects(DerivativeVar, descend_into=True):
            dsets = d.get_continuousset_list()
            for i in ComponentSet(dsets):
                if currentds is None or i.name == currentds:
                    oldexpr = d.get_derivative_expression()
                    loc = d.get_state_var()._contset[i]
                    count = dsets.count(i)
                    if count >= 3:
                        raise DAE_Error(
                            "Error discretizing '%s' with respect to '%s'. "
                            "Current implementation only allows for taking the"
                            " first or second derivative with respect to a "
                            "particular ContinuousSet" % (d.name, i.name))
                    scheme = self._scheme[count - 1]

                    newexpr = create_partial_expression(
                        scheme, oldexpr, i, loc)
                    d.set_derivative_expression(newexpr)
                    if self._scheme_name == 'LAGRANGE-LEGENDRE':
                        # Add continuity equations to DerivativeVar's parent
                        #  block
                        add_continuity_equations(d.parent_block(), d, i, loc)

            # Reclassify DerivativeVar if all indexing ContinuousSets have
            # been discretized. Add discretization equations to the
            # DerivativeVar's parent block.
            if d.is_fully_discretized():
                add_discretization_equations(d.parent_block(), d)
                d.parent_block().reclassify_component_type(d, Var)

                # Keep track of any reclassified DerivativeVar components so
                # that the Simulator can easily identify them if the model
                # is simulated after discretization
                # TODO: Update the discretization transformations to use
                # a Block to add things to the model and store discretization
                # information. Using a list for now because the simulator
                # does not yet support models containing active Blocks
                reclassified_list = getattr(
                    block, '_pyomo_dae_reclassified_derivativevars', None)
                if reclassified_list is None:
                    block._pyomo_dae_reclassified_derivativevars = list()
                    reclassified_list = \
                        block._pyomo_dae_reclassified_derivativevars

                reclassified_list.append(d)

        # Reclassify Integrals if all ContinuousSets have been discretized
        if block_fully_discretized(block):

            if block.contains_component(Integral):
                for i in block.component_objects(Integral, descend_into=True):
                    i.parent_block().reclassify_component_type(i, Expression)
                    # TODO: The following reproduces the old behavior of
                    # "reconstruct()".  We should come up with an
                    # implementation that does not rely on manipulating
                    # private attributes
                    i.clear()
                    i._constructed = False
                    i.construct()
                # If a model contains integrals they are most likely to appear
                # in the objective function which will need to be reconstructed
                # after the model is discretized.
                for k in block.component_objects(Objective, descend_into=True):
                    # TODO: check this, reconstruct might not work
                    # TODO: The following reproduces the old behavior of
                    # "reconstruct()".  We should come up with an
                    # implementation that does not rely on manipulating
                    # private attributes
                    k.clear()
                    k._constructed = False
                    k.construct()

    def reduce_collocation_points(self,
                                  instance,
                                  var=None,
                                  ncp=None,
                                  contset=None):
        """
        This method will add additional constraints to a model to reduce the
        number of free collocation points (degrees of freedom) for a particular
        variable.

        Parameters
        ----------
        instance : Pyomo model
            The discretized Pyomo model to add constraints to

        var : ``pyomo.environ.Var``
            The Pyomo variable for which the degrees of freedom will be reduced

        ncp : int
            The new number of free collocation points for `var`. Must be
            less that the number of collocation points used in discretizing
            the model.

        contset : ``pyomo.dae.ContinuousSet``
            The :py:class:`ContinuousSet<pyomo.dae.ContinuousSet>` that was
            discretized and for which the `var` will have a reduced number
            of degrees of freedom

        """
        if contset is None:
            raise TypeError("A continuous set must be specified using the "
                            "keyword 'contset'")
        if contset.ctype is not ContinuousSet:
            raise TypeError("The component specified using the 'contset' "
                            "keyword must be a ContinuousSet")
        ds = contset

        if len(self._ncp) == 0:
            raise RuntimeError("This method should only be called after using "
                               "the apply() method to discretize the model")
        elif None in self._ncp:
            tot_ncp = self._ncp[None]
        elif ds.name in self._ncp:
            tot_ncp = self._ncp[ds.name]
        else:
            raise ValueError("ContinuousSet '%s' has not been discretized, "
                             "please call the apply_to() method with this "
                             "ContinuousSet to discretize it before calling "
                             "this method" % ds.name)

        if var is None:
            raise TypeError("A variable must be specified")
        if var.ctype is not Var:
            raise TypeError("The component specified using the 'var' keyword "
                            "must be a variable")

        if ncp is None:
            raise TypeError(
                "The number of collocation points must be specified")
        if ncp <= 0:
            raise ValueError(
                "The number of collocation points must be at least 1")
        if ncp > tot_ncp:
            raise ValueError("The number of collocation points used to "
                             "interpolate an individual variable must be less "
                             "than the number used to discretize the original "
                             "model")
        if ncp == tot_ncp:
            # Nothing to be done
            return instance

        # Check to see if the continuousset is an indexing set of the variable
        if var.dim() == 0:
            raise IndexError("ContinuousSet '%s' is not an indexing set of"
                             " the variable '%s'" % (ds.name, var.name))
        varidx = var.index_set()
        if not hasattr(varidx, 'set_tuple'):
            if ds is not varidx:
                raise IndexError("ContinuousSet '%s' is not an indexing set of"
                                 " the variable '%s'" % (ds.name, var.name))
        elif ds not in varidx.set_tuple:
            raise IndexError("ContinuousSet '%s' is not an indexing set of the"
                             " variable '%s'" % (ds.name, var.name))

        if var.name in self._reduced_cp:
            temp = self._reduced_cp[var.name]
            if ds.name in temp:
                raise RuntimeError("Variable '%s' has already been constrained"
                                   " to a reduced number of collocation points"
                                   " over ContinuousSet '%s'.")
            else:
                temp[ds.name] = ncp
        else:
            self._reduced_cp[var.name] = {ds.name: ncp}

        # TODO: Use unique_component_name for this
        list_name = var.local_name + "_interpolation_constraints"

        instance.add_component(list_name, ConstraintList())
        conlist = instance.find_component(list_name)

        t = list(ds)
        fe = ds._fe
        info = get_index_information(var, ds)
        tmpidx = info['non_ds']
        idx = info['index function']

        # Iterate over non_ds indices
        for n in tmpidx:
            # Iterate over finite elements
            for i in range(0, len(fe) - 1):
                # Iterate over collocation points
                for k in range(1, tot_ncp - ncp + 1):
                    if ncp == 1:
                        # Constant over each finite element
                        conlist.add(
                            var[idx(n, i, k)] == var[idx(n, i, tot_ncp)])
                    else:
                        tmp = ds.ord(fe[i]) - 1
                        tmp2 = ds.ord(fe[i + 1]) - 1
                        ti = t[tmp + k]
                        tfit = t[tmp2 - ncp + 1:tmp2 + 1]
                        coeff = self._interpolation_coeffs(ti, tfit)
                        conlist.add(var[idx(n, i, k)] == sum(
                            var[idx(n, i, j)] * next(coeff)
                            for j in range(tot_ncp - ncp + 1, tot_ncp + 1)))

        return instance

    def _interpolation_coeffs(self, ti, tfit):

        for i in tfit:
            l = 1
            for j in tfit:
                if i != j:
                    l = l * (ti - j) / (i - j)
            yield l
Esempio n. 19
0
class StateTestBlockData(StateBlockData):
    CONFIG = ConfigBlock(implicit=True)

    def build(self):
        super(StateTestBlockData, self).build()

        self.flow_vol = Var(initialize=20, units=units.m**3 / units.s)
        self.flow_mol_phase_comp = Var(self.params.phase_list,
                                       self.params.component_list,
                                       initialize=2,
                                       units=units.mol / units.s)
        self.test_var = Var(initialize=1)
        self.enthalpy_flow = Var(initialize=1, units=units.J / units.s)
        self.energy_dens = Var(initialize=1, units=units.J / units.m**3)
        self.material_flow_mol = Var(initialize=1, units=units.mol / units.s)
        self.material_dens_mol = Var(initialize=1,
                                     units=units.mol / units.m**3)
        self.material_flow_mass = Var(initialize=1, units=units.kg / units.s)
        self.material_dens_mass = Var(initialize=1,
                                      units=units.kg / units.m**3)
        self.pressure = Var(initialize=1e5, units=units.Pa)
        self.temperature = Var(initialize=300, units=units.K)

        self.enth_mol = Var(initialize=10000, units=units.J / units.mol)

        self.gibbs_mol_phase_comp = Var(self.params.phase_list,
                                        self.params.component_list,
                                        initialize=50,
                                        units=units.J / units.mol)
        self.entr_mol = Var(initialize=1000,
                            units=units.J / units.mol / units.K)

        self.mole_frac_phase_comp = Var(self.params.phase_list,
                                        self.params.component_list,
                                        initialize=0.5)

    def get_material_flow_terms(b, p, j):
        if b.config.parameters.basis_switch == 2:
            return b.material_flow_mass
        else:
            return b.material_flow_mol

    def get_material_density_terms(b, p, j):
        if b.config.parameters.basis_switch == 2:
            return b.material_dens_mass
        else:
            return b.material_dens_mol

    def get_enthalpy_flow_terms(b, p):
        return b.enthalpy_flow

    def get_energy_density_terms(b, p):
        return b.energy_dens

    def model_check(self):
        self.check = True

    def get_material_flow_basis(b):
        if b.config.parameters.basis_switch == 1:
            return MaterialFlowBasis.molar
        elif b.config.parameters.basis_switch == 2:
            return MaterialFlowBasis.mass
        else:
            return MaterialFlowBasis.other

    def default_material_balance_type(self):
        if self.params.default_balance_switch == 1:
            return MaterialBalanceType.componentPhase
        else:
            raise NotImplementedError

    def default_energy_balance_type(self):
        if self.params.default_balance_switch == 1:
            return EnergyBalanceType.enthalpyTotal
        else:
            raise NotImplementedError

    def define_state_vars(self):
        return {
            "component_flow_phase": self.flow_mol_phase_comp,
            "temperature": self.temperature,
            "pressure": self.pressure
        }
Esempio n. 20
0
class BigM_Transformation(Transformation):
    """Relax disjunctive model using big-M terms.

    Relaxes a disjunctive model into an algebraic model by adding Big-M
    terms to all disjunctive constraints.

    This transformation accepts the following keyword arguments:
        bigM: A user-specified value (or dict) of M values to use (see below)
        targets: the targets to transform [default: the instance]

    M values are determined as follows:
       1) if the constraint appears in the bigM argument dict
       2) if the constraint parent_component appears in the bigM
          argument dict
       3) if any block which is an ancestor to the constraint appears in
          the bigM argument dict
       3) if 'None' is in the bigM argument dict
       4) if the constraint or the constraint parent_component appear in
          a BigM Suffix attached to any parent_block() beginning with the
          constraint's parent_block and moving up to the root model.
       5) if None appears in a BigM Suffix attached to any
          parent_block() between the constraint and the root model.
       6) if the constraint is linear, estimate M using the variable bounds

    M values may be a single value or a 2-tuple specifying the M for the
    lower bound and the upper bound of the constraint body.

    Specifying "bigM=N" is automatically mapped to "bigM={None: N}".

    The transformation will create a new Block with a unique
    name beginning "_pyomo_gdp_bigm_reformulation".  That Block will
    contain an indexed Block named "relaxedDisjuncts", which will hold
    the relaxed disjuncts.  This block is indexed by an integer
    indicating the order in which the disjuncts were relaxed.
    Each block has a dictionary "_constraintMap":

        'srcConstraints': ComponentMap(<transformed constraint>:
                                       <src constraint>)
        'transformedConstraints': ComponentMap(<src constraint>:
                                               <transformed constraint>)

    All transformed Disjuncts will have a pointer to the block their transformed
    constraints are on, and all transformed Disjunctions will have a
    pointer to the corresponding OR or XOR constraint.

    """

    CONFIG = ConfigBlock("gdp.bigm")
    CONFIG.declare('targets', ConfigValue(
        default=None,
        domain=target_list,
        description="target or list of targets that will be relaxed",
        doc="""

        This specifies the list of components to relax. If None (default), the
        entire model is transformed. Note that if the transformation is done out
        of place, the list of targets should be attached to the model before it
        is cloned, and the list will specify the targets on the cloned
        instance."""
    ))
    CONFIG.declare('bigM', ConfigValue(
        default=None,
        domain=_to_dict,
        description="Big-M value used for constraint relaxation",
        doc="""

        A user-specified value, dict, or ComponentMap of M values that override
        M-values found through model Suffixes or that would otherwise be
        calculated using variable domains."""
    ))
    CONFIG.declare('assume_fixed_vars_permanent', ConfigValue(
        default=False,
        domain=bool,
        description="Boolean indicating whether or not to transform so that the "
        "the transformed model will still be valid when fixed Vars are unfixed.",
        doc="""
        This is only relevant when the transformation will be estimating values
        for M. If True, the transformation will calculate M values assuming that
        fixed variables will always be fixed to their current values. This means
        that if a fixed variable is unfixed after transformation, the
        transformed model is potentially no longer valid. By default, the
        transformation will assume fixed variables could be unfixed in the
        future and will use their bounds to calculate the M value rather than
        their value. Note that this could make for a weaker LP relaxation
        while the variables remain fixed.
        """
    ))

    def __init__(self):
        """Initialize transformation object."""
        super(BigM_Transformation, self).__init__()
        self.handlers = {
            Constraint:  self._transform_constraint,
            Var:         False, # Note that if a Var appears on a Disjunct, we
                                # still treat its bounds as global. If the
                                # intent is for its bounds to be on the
                                # disjunct, it should be declared with no bounds
                                # and the bounds should be set in constraints on
                                # the Disjunct.
            BooleanVar:  False,
            Connector:   False,
            Expression:  False,
            Suffix:      False,
            Param:       False,
            Set:         False,
            SetOf:       False,
            RangeSet:    False,
            Disjunction: self._warn_for_active_disjunction,
            Disjunct:    self._warn_for_active_disjunct,
            Block:       self._transform_block_on_disjunct,
            LogicalConstraint: self._warn_for_active_logical_statement,
            ExternalFunction: False,
        }
        self._generate_debug_messages = False

    def _get_bigm_suffix_list(self, block, stopping_block=None):
        # Note that you can only specify suffixes on BlockData objects or
        # SimpleBlocks. Though it is possible at this point to stick them
        # on whatever components you want, we won't pick them up.
        suffix_list = []

        # go searching above block in the tree, stop when we hit stopping_block
        # (This is so that we can search on each Disjunct once, but get any
        # information between a constraint and its Disjunct while transforming
        # the constraint).
        while block is not stopping_block:
            bigm = block.component('BigM')
            if type(bigm) is Suffix:
                suffix_list.append(bigm)
            block = block.parent_block()

        return suffix_list

    def _get_bigm_arg_list(self, bigm_args, block):
        # Gather what we know about blocks from args exactly once. We'll still
        # check for constraints in the moment, but if that fails, we've
        # preprocessed the time-consuming part of traversing up the tree.
        arg_list = []
        if bigm_args is None:
            return arg_list
        while block is not None:
            if block in bigm_args:
                arg_list.append({block: bigm_args[block]})
            block = block.parent_block()
        return arg_list

    def _apply_to(self, instance, **kwds):
        assert not NAME_BUFFER
        self._generate_debug_messages = is_debug_set(logger)
        self.used_args = ComponentMap() # If everything was sure to go well,
                                        # this could be a dictionary. But if
                                        # someone messes up and gives us a Var
                                        # as a key in bigMargs, I need the error
                                        # not to be when I try to put it into
                                        # this map!
        try:
            self._apply_to_impl(instance, **kwds)
        finally:
            # Clear the global name buffer now that we are done
            NAME_BUFFER.clear()
            # same for our bookkeeping about what we used from bigM arg dict
            self.used_args.clear()

    def _apply_to_impl(self, instance, **kwds):
        config = self.CONFIG(kwds.pop('options', {}))

        # We will let args override suffixes and estimate as a last
        # resort. More specific args/suffixes override ones anywhere in
        # the tree. Suffixes lower down in the tree override ones higher
        # up.
        if 'default_bigM' in kwds:
            deprecation_warning("the 'default_bigM=' argument has been "
                                "replaced by 'bigM='", version='5.4')
            config.bigM = kwds.pop('default_bigM')

        config.set_value(kwds)
        bigM = config.bigM
        self.assume_fixed_vars_permanent = config.assume_fixed_vars_permanent

        targets = config.targets
        if targets is None:
            targets = (instance, )
        # We need to check that all the targets are in fact on instance. As we
        # do this, we will use the set below to cache components we know to be
        # in the tree rooted at instance.
        knownBlocks = {}
        for t in targets:
            # check that t is in fact a child of instance
            if not is_child_of(parent=instance, child=t,
                               knownBlocks=knownBlocks):
                raise GDP_Error(
                    "Target '%s' is not a component on instance '%s'!"
                    % (t.name, instance.name))
            elif t.ctype is Disjunction:
                if t.is_indexed():
                    self._transform_disjunction(t, bigM)
                else:
                    self._transform_disjunctionData( t, bigM, t.index())
            elif t.ctype in (Block, Disjunct):
                if t.is_indexed():
                    self._transform_block(t, bigM)
                else:
                    self._transform_blockData(t, bigM)
            else:
                raise GDP_Error(
                    "Target '%s' was not a Block, Disjunct, or Disjunction. "
                    "It was of type %s and can't be transformed."
                    % (t.name, type(t)))

        # issue warnings about anything that was in the bigM args dict that we
        # didn't use
        if bigM is not None:
            unused_args = ComponentSet(bigM.keys()) - \
                          ComponentSet(self.used_args.keys())
            if len(unused_args) > 0:
                warning_msg = ("Unused arguments in the bigM map! "
                               "These arguments were not used by the "
                               "transformation:\n")
                for component in unused_args:
                    if hasattr(component, 'name'):
                        warning_msg += "\t%s\n" % component.name
                    else:
                        warning_msg += "\t%s\n" % component
                logger.warning(warning_msg)

    def _add_transformation_block(self, instance):
        # make a transformation block on instance to put transformed disjuncts
        # on
        transBlockName = unique_component_name(
            instance,
            '_pyomo_gdp_bigm_reformulation')
        transBlock = Block()
        instance.add_component(transBlockName, transBlock)
        transBlock.relaxedDisjuncts = Block(NonNegativeIntegers)
        transBlock.lbub = Set(initialize=['lb', 'ub'])

        return transBlock

    def _transform_block(self, obj, bigM):
        for i in sorted(obj.keys()):
            self._transform_blockData(obj[i], bigM)

    def _transform_blockData(self, obj, bigM):
        # Transform every (active) disjunction in the block
        for disjunction in obj.component_objects(
                Disjunction,
                active=True,
                sort=SortComponents.deterministic,
                descend_into=(Block, Disjunct),
                descent_order=TraversalStrategy.PostfixDFS):
            self._transform_disjunction(disjunction, bigM)

    def _add_xor_constraint(self, disjunction, transBlock):
        # Put the disjunction constraint on the transformation block and
        # determine whether it is an OR or XOR constraint.

        # We never do this for just a DisjunctionData because we need to know
        # about the index set of its parent component (so that we can make the
        # index of this constraint match). So if we called this on a
        # DisjunctionData, we did something wrong.
        assert isinstance(disjunction, Disjunction)

        # first check if the constraint already exists
        if disjunction._algebraic_constraint is not None:
            return disjunction._algebraic_constraint()

        # add the XOR (or OR) constraints to parent block (with unique name)
        # It's indexed if this is an IndexedDisjunction, not otherwise
        orC = Constraint(disjunction.index_set()) if \
            disjunction.is_indexed() else Constraint()
        # The name used to indicate if there were OR or XOR disjunctions,
        # however now that Disjunctions are allowed to mix the state we
        # can no longer make that distinction in the name.
        #    nm = '_xor' if xor else '_or'
        nm = '_xor'
        orCname = unique_component_name( transBlock, disjunction.getname(
            fully_qualified=True, name_buffer=NAME_BUFFER) + nm)
        transBlock.add_component(orCname, orC)
        disjunction._algebraic_constraint = weakref_ref(orC)

        return orC

    def _transform_disjunction(self, obj, bigM):
        if not obj.active:
            return

        # if this is an IndexedDisjunction we have seen in a prior call to the
        # transformation, we already have a transformation block for it. We'll
        # use that.
        if obj._algebraic_constraint is not None:
            transBlock = obj._algebraic_constraint().parent_block()
        else:
            transBlock = self._add_transformation_block(obj.parent_block())

        # relax each of the disjunctionDatas
        for i in sorted(obj.keys()):
            self._transform_disjunctionData(obj[i], bigM, i, transBlock)

        # deactivate so the writers don't scream
        obj.deactivate()

    def _transform_disjunctionData(self, obj, bigM, index, transBlock=None):
        if not obj.active:
            return  # Do not process a deactivated disjunction
        # We won't have these arguments if this got called straight from
        # targets. But else, we created them earlier, and have just been passing
        # them through.
        if transBlock is None:
            # It's possible that we have already created a transformation block
            # for another disjunctionData from this same container. If that's
            # the case, let's use the same transformation block. (Else it will
            # be really confusing that the XOR constraint goes to that old block
            # but we create a new one here.)
            if obj.parent_component()._algebraic_constraint is not None:
                transBlock = obj.parent_component()._algebraic_constraint().\
                             parent_block()
            else:
                transBlock = self._add_transformation_block(obj.parent_block())
        # create or fetch the xor constraint
        xorConstraint = self._add_xor_constraint(obj.parent_component(),
                                                 transBlock)

        xor = obj.xor
        or_expr = 0
        # Just because it's unlikely this is what someone meant to do...
        if len(obj.disjuncts) == 0:
            raise GDP_Error("Disjunction '%s' is empty. This is "
                            "likely indicative of a modeling error."  %
                            obj.getname(fully_qualified=True,
                                        name_buffer=NAME_BUFFER))
        for disjunct in obj.disjuncts:
            or_expr += disjunct.indicator_var
            # make suffix list. (We don't need it until we are
            # transforming constraints, but it gets created at the
            # disjunct level, so more efficient to make it here and
            # pass it down.)
            suffix_list = self._get_bigm_suffix_list(disjunct)
            arg_list = self._get_bigm_arg_list(bigM, disjunct)
            # relax the disjunct
            self._transform_disjunct(disjunct, transBlock, bigM, arg_list,
                                     suffix_list)

        # add or (or xor) constraint
        if xor:
            xorConstraint[index] = or_expr == 1
        else:
            xorConstraint[index] = or_expr >= 1
        # Mark the DisjunctionData as transformed by mapping it to its XOR
        # constraint.
        obj._algebraic_constraint = weakref_ref(xorConstraint[index])

        # and deactivate for the writers
        obj.deactivate()

    def _transform_disjunct(self, obj, transBlock, bigM, arg_list, suffix_list):
        # deactivated -> either we've already transformed or user deactivated
        if not obj.active:
            if obj.indicator_var.is_fixed():
                if value(obj.indicator_var) == 0:
                    # The user cleanly deactivated the disjunct: there
                    # is nothing for us to do here.
                    return
                else:
                    raise GDP_Error(
                        "The disjunct '%s' is deactivated, but the "
                        "indicator_var is fixed to %s. This makes no sense."
                        % ( obj.name, value(obj.indicator_var) ))
            if obj._transformation_block is None:
                raise GDP_Error(
                    "The disjunct '%s' is deactivated, but the "
                    "indicator_var is not fixed and the disjunct does not "
                    "appear to have been relaxed. This makes no sense. "
                    "(If the intent is to deactivate the disjunct, fix its "
                    "indicator_var to 0.)"
                    % ( obj.name, ))

        if obj._transformation_block is not None:
            # we've transformed it, which means this is the second time it's
            # appearing in a Disjunction
            raise GDP_Error(
                    "The disjunct '%s' has been transformed, but a disjunction "
                    "it appears in has not. Putting the same disjunct in "
                    "multiple disjunctions is not supported." % obj.name)

        # add reference to original disjunct on transformation block
        relaxedDisjuncts = transBlock.relaxedDisjuncts
        relaxationBlock = relaxedDisjuncts[len(relaxedDisjuncts)]
        # we will keep a map of constraints (hashable, ha!) to a tuple to
        # indicate what their M value is and where it came from, of the form:
        # ((lower_value, lower_source, lower_key), (upper_value, upper_source,
        # upper_key)), where the first tuple is the information for the lower M,
        # the second tuple is the info for the upper M, source is the Suffix or
        # argument dictionary and None if the value was calculated, and key is
        # the key in the Suffix or argument dictionary, and None if it was
        # calculated. (Note that it is possible the lower or upper is
        # user-specified and the other is not, hence the need to store
        # information for both.)
        relaxationBlock.bigm_src = {}
        relaxationBlock.localVarReferences = Block()
        obj._transformation_block = weakref_ref(relaxationBlock)
        relaxationBlock._srcDisjunct = weakref_ref(obj)

        # This is crazy, but if the disjunction has been previously
        # relaxed, the disjunct *could* be deactivated.  This is a big
        # deal for Hull, as it uses the component_objects /
        # component_data_objects generators.  For BigM, that is OK,
        # because we never use those generators with active=True.  I am
        # only noting it here for the future when someone (me?) is
        # comparing the two relaxations.
        #
        # Transform each component within this disjunct
        self._transform_block_components(obj, obj, bigM, arg_list, suffix_list)

        # deactivate disjunct to keep the writers happy
        obj._deactivate_without_fixing_indicator()

    def _transform_block_components(self, block, disjunct, bigM, arg_list,
                                    suffix_list):
        # Find all the variables declared here (including the indicator_var) and
        # add a reference on the transformation block so these will be
        # accessible when the Disjunct is deactivated. We don't descend into
        # Disjuncts because we'll just reference the references which are
        # already on their transformation blocks.
        disjunctBlock = disjunct._transformation_block()
        varRefBlock = disjunctBlock.localVarReferences
        for v in block.component_objects(Var, descend_into=Block, active=None):
            varRefBlock.add_component(unique_component_name(
                varRefBlock, v.getname(fully_qualified=True,
                                       name_buffer=NAME_BUFFER)), Reference(v))

        # Now need to find any transformed disjunctions that might be here
        # because we need to move their transformation blocks up onto the parent
        # block before we transform anything else on this block
        destinationBlock = disjunctBlock.parent_block()
        for obj in block.component_data_objects(
                Disjunction,
                sort=SortComponents.deterministic,
                descend_into=(Block)):
            if obj.algebraic_constraint is None:
                # This could be bad if it's active since that means its
                # untransformed, but we'll wait to yell until the next loop
                continue
            # get this disjunction's relaxation block.
            transBlock = obj.algebraic_constraint().parent_block()

            # move transBlock up to parent component
            self._transfer_transBlock_data(transBlock, destinationBlock)
            # we leave the transformation block because it still has the XOR
            # constraints, which we want to be on the parent disjunct.

        # Now look through the component map of block and transform everything
        # we have a handler for. Yell if we don't know how to handle it. (Note
        # that because we only iterate through active components, this means
        # non-ActiveComponent types cannot have handlers.)
        for obj in block.component_objects(active=True, descend_into=False):
            handler = self.handlers.get(obj.ctype, None)
            if not handler:
                if handler is None:
                    raise GDP_Error(
                        "No BigM transformation handler registered "
                        "for modeling components of type %s. If your "
                        "disjuncts contain non-GDP Pyomo components that "
                        "require transformation, please transform them first."
                        % obj.ctype)
                continue
            # obj is what we are transforming, we pass disjunct
            # through so that we will have access to the indicator
            # variables down the line.
            handler(obj, disjunct, bigM, arg_list, suffix_list)

    def _transfer_transBlock_data(self, fromBlock, toBlock):
        # We know that we have a list of transformed disjuncts on both. We need
        # to move those over. We know the XOR constraints are on the block, and
        # we need to leave those on the disjunct.
        disjunctList = toBlock.relaxedDisjuncts
        to_delete = []
        for idx, disjunctBlock in fromBlock.relaxedDisjuncts.items():
            newblock = disjunctList[len(disjunctList)]
            newblock.transfer_attributes_from(disjunctBlock)

            # update the mappings
            original = disjunctBlock._srcDisjunct()
            original._transformation_block = weakref_ref(newblock)
            newblock._srcDisjunct = weakref_ref(original)

            # save index of what we just moved so that we can delete it
            to_delete.append(idx)

        # delete everything we moved.
        for idx in to_delete:
            del fromBlock.relaxedDisjuncts[idx]

        # Note that we could handle other components here if we ever needed
        # to, but we control what is on the transformation block and
        # currently everything is on the blocks that we just moved...

    def _warn_for_active_disjunction(self, disjunction, disjunct, bigMargs,
                                     arg_list, suffix_list):
        _warn_for_active_disjunction(disjunction, disjunct, NAME_BUFFER)

    def _warn_for_active_disjunct(self, innerdisjunct, outerdisjunct, bigMargs,
                                  arg_list, suffix_list):
        _warn_for_active_disjunct(innerdisjunct, outerdisjunct, NAME_BUFFER)

    def _warn_for_active_logical_statement(
            self, logical_statment, disjunct, infodict, bigMargs, suffix_list):
        _warn_for_active_logical_constraint(logical_statment, disjunct, NAME_BUFFER)

    def _transform_block_on_disjunct(self, block, disjunct, bigMargs, arg_list,
                                     suffix_list):
        # We look through everything on the component map of the block
        # and transform it just as we would if it was on the disjunct
        # directly.  (We are passing the disjunct through so that when
        # we find constraints, _xform_constraint will have access to
        # the correct indicator variable.)
        for i in sorted(block.keys()):
            self._transform_block_components( block[i], disjunct, bigMargs,
                                              arg_list, suffix_list)

    def _get_constraint_map_dict(self, transBlock):
        if not hasattr(transBlock, "_constraintMap"):
            transBlock._constraintMap = {
                'srcConstraints': ComponentMap(),
                'transformedConstraints': ComponentMap()}
        return transBlock._constraintMap

    def _convert_M_to_tuple(self, M, constraint_name):
        if not isinstance(M, (tuple, list)):
            if M is None:
                M = (None, None)
            else:
                try:
                    M = (-M, M)
                except:
                    logger.error("Error converting scalar M-value %s "
                                 "to (-M,M).  Is %s not a numeric type?"
                                 % (M, type(M)))
                    raise
        if len(M) != 2:
            raise GDP_Error("Big-M %s for constraint %s is not of "
                            "length two. "
                            "Expected either a single value or "
                            "tuple or list of length two for M."
                            % (str(M), constraint_name))

        return M

    def _transform_constraint(self, obj, disjunct, bigMargs, arg_list,
                              disjunct_suffix_list):
        # add constraint to the transformation block, we'll transform it there.
        transBlock = disjunct._transformation_block()
        bigm_src = transBlock.bigm_src
        constraintMap = self._get_constraint_map_dict(transBlock)

        disjunctionRelaxationBlock = transBlock.parent_block()
        # Though rare, it is possible to get naming conflicts here
        # since constraints from all blocks are getting moved onto the
        # same block. So we get a unique name
        cons_name = obj.getname(fully_qualified=True, name_buffer=NAME_BUFFER)
        name = unique_component_name(transBlock, cons_name)

        if obj.is_indexed():
            newConstraint = Constraint(obj.index_set(),
                                       disjunctionRelaxationBlock.lbub)
            # we map the container of the original to the container of the
            # transformed constraint. Don't do this if obj is a SimpleConstraint
            # because we will treat that like a _ConstraintData and map to a
            # list of transformed _ConstraintDatas
            constraintMap['transformedConstraints'][obj] = newConstraint
        else:
            newConstraint = Constraint(disjunctionRelaxationBlock.lbub)
        transBlock.add_component(name, newConstraint)
        # add mapping of transformed constraint to original constraint
        constraintMap['srcConstraints'][newConstraint] = obj

        for i in sorted(obj.keys()):
            c = obj[i]
            if not c.active:
                continue

            lower = (None, None, None)
            upper = (None, None, None)

            # first, we see if an M value was specified in the arguments.
            # (This returns None if not)
            lower, upper = self._get_M_from_args(c, bigMargs, arg_list, lower,
                                                 upper)
            M = (lower[0], upper[0])
            
            if self._generate_debug_messages:
                _name = obj.getname(
                    fully_qualified=True, name_buffer=NAME_BUFFER)
                logger.debug("GDP(BigM): The value for M for constraint '%s' "
                             "from the BigM argument is %s." % (cons_name,
                                                                str(M)))

            # if we didn't get something we need from args, try suffixes:
            if (M[0] is None and c.lower is not None) or \
               (M[1] is None and c.upper is not None):
                # first get anything parent to c but below disjunct
                suffix_list = self._get_bigm_suffix_list(c.parent_block(),
                                                         stopping_block=disjunct)
                # prepend that to what we already collected for the disjunct.
                suffix_list.extend(disjunct_suffix_list)
                lower, upper = self._update_M_from_suffixes(c, suffix_list,
                                                            lower, upper)
                M = (lower[0], upper[0])

            if self._generate_debug_messages:
                _name = obj.getname(
                    fully_qualified=True, name_buffer=NAME_BUFFER)
                logger.debug("GDP(BigM): The value for M for constraint '%s' "
                             "after checking suffixes is %s." % (cons_name,
                                                                 str(M)))

            if c.lower is not None and M[0] is None:
                M = (self._estimate_M(c.body, name)[0] - c.lower, M[1])
                lower = (M[0], None, None)
            if c.upper is not None and M[1] is None:
                M = (M[0], self._estimate_M(c.body, name)[1] - c.upper)
                upper = (M[1], None, None)

            if self._generate_debug_messages:
                _name = obj.getname(
                    fully_qualified=True, name_buffer=NAME_BUFFER)
                logger.debug("GDP(BigM): The value for M for constraint '%s' "
                             "after estimating (if needed) is %s." %
                             (cons_name, str(M)))

            # save the source information
            bigm_src[c] = (lower, upper)

            # Handle indices for both SimpleConstraint and IndexedConstraint
            if i.__class__ is tuple:
                i_lb = i + ('lb',)
                i_ub = i + ('ub',)
            elif obj.is_indexed():
                i_lb = (i, 'lb',)
                i_ub = (i, 'ub',)
            else:
                i_lb = 'lb'
                i_ub = 'ub'

            if c.lower is not None:
                if M[0] is None:
                    raise GDP_Error("Cannot relax disjunctive constraint '%s' "
                                    "because M is not defined." % name)
                M_expr = M[0] * (1 - disjunct.indicator_var)
                newConstraint.add(i_lb, c.lower <= c. body - M_expr)
                constraintMap[
                    'transformedConstraints'][c] = [newConstraint[i_lb]]
                constraintMap['srcConstraints'][newConstraint[i_lb]] = c
            if c.upper is not None:
                if M[1] is None:
                    raise GDP_Error("Cannot relax disjunctive constraint '%s' "
                                    "because M is not defined." % name)
                M_expr = M[1] * (1 - disjunct.indicator_var)
                newConstraint.add(i_ub, c.body - M_expr <= c.upper)
                transformed = constraintMap['transformedConstraints'].get(c)
                if transformed is not None:
                    constraintMap['transformedConstraints'][
                        c].append(newConstraint[i_ub])
                else:
                    constraintMap[
                        'transformedConstraints'][c] = [newConstraint[i_ub]]
                constraintMap['srcConstraints'][newConstraint[i_ub]] = c

            # deactivate because we relaxed
            c.deactivate()

    def _process_M_value(self, m, lower, upper, need_lower, need_upper, src,
                         key, constraint_name, from_args=False):
        m = self._convert_M_to_tuple(m, constraint_name)
        if need_lower and m[0] is not None:
            if from_args:
                self.used_args[key] = m
            lower = (m[0], src, key)
            need_lower = False
        if need_upper and m[1] is not None:
            if from_args:
                self.used_args[key] = m
            upper = (m[1], src, key)
            need_upper = False
        return lower, upper, need_lower, need_upper

    def _get_M_from_args(self, constraint, bigMargs, arg_list, lower, upper):
        # check args: we first look in the keys for constraint and
        # constraintdata. In the absence of those, we traverse up the blocks,
        # and as a last resort check for a value for None
        if bigMargs is None:
            return (lower, upper)

        # since we check for args first, we know lower[0] and upper[0] are both
        # None
        need_lower = constraint.lower is not None
        need_upper = constraint.upper is not None
        constraint_name = constraint.getname(fully_qualified=True,
                                             name_buffer=NAME_BUFFER)

        # check for the constraint itself and its container
        parent = constraint.parent_component()
        if constraint in bigMargs:
            m = bigMargs[constraint]
            (lower, upper, 
             need_lower, need_upper) = self._process_M_value(m, lower, upper,
                                                             need_lower,
                                                             need_upper,
                                                             bigMargs,
                                                             constraint,
                                                             constraint_name,
                                                             from_args=True)
            if not need_lower and not need_upper:
                return lower, upper
        elif parent in bigMargs:
            m = bigMargs[parent]
            (lower, upper, 
             need_lower, need_upper) = self._process_M_value(m, lower, upper,
                                                             need_lower,
                                                             need_upper,
                                                             bigMargs, parent,
                                                             constraint_name,
                                                             from_args=True)
            if not need_lower and not need_upper:
                return lower, upper

        # use the precomputed traversal up the blocks
        for arg in arg_list:
            for block, val in arg.items():
                (lower, upper, 
                 need_lower, need_upper) = self._process_M_value(val, lower,
                                                                 upper,
                                                                 need_lower,
                                                                 need_upper,
                                                                 bigMargs,
                                                                 block,
                                                                 constraint_name,
                                                                 from_args=True)
                if not need_lower and not need_upper:
                    return lower, upper

        # last check for value for None!
        if None in bigMargs:
            m = bigMargs[None]
            (lower, upper, 
             need_lower, need_upper) = self._process_M_value(m, lower, upper,
                                                             need_lower,
                                                             need_upper,
                                                             bigMargs, None,
                                                             constraint_name,
                                                             from_args=True)
            if not need_lower and not need_upper:
                return lower, upper

        return lower, upper

    def _update_M_from_suffixes(self, constraint, suffix_list, lower, upper):
        # It's possible we found half the answer in args, but we are still
        # looking for half the answer.
        need_lower = constraint.lower is not None and lower[0] is None
        need_upper = constraint.upper is not None and upper[0] is None
        constraint_name = constraint.getname(fully_qualified=True,
                                             name_buffer=NAME_BUFFER)
        M = None
        # first we check if the constraint or its parent is a key in any of the
        # suffix lists
        for bigm in suffix_list:
            if constraint in bigm:
                M = bigm[constraint]
                (lower, upper, 
                 need_lower, need_upper) = self._process_M_value(M, lower,
                                                                 upper,
                                                                 need_lower,
                                                                 need_upper,
                                                                 bigm,
                                                                 constraint,
                                                                 constraint_name)
                if not need_lower and not need_upper:
                    return lower, upper

            # if c is indexed, check for the parent component
            if constraint.parent_component() in bigm:
                parent = constraint.parent_component()
                M = bigm[parent]
                (lower, upper, 
                 need_lower, need_upper) = self._process_M_value(M, lower,
                                                                 upper,
                                                                 need_lower,
                                                                 need_upper,
                                                                 bigm, parent,
                                                                 constraint_name)
                if not need_lower and not need_upper:
                    return lower, upper

        # if we didn't get an M that way, traverse upwards through the blocks
        # and see if None has a value on any of them.
        if M is None:
            for bigm in suffix_list:
                if None in bigm:
                    M = bigm[None]
                    (lower, upper, 
                     need_lower, 
                     need_upper) = self._process_M_value(M, lower, upper,
                                                         need_lower, need_upper,
                                                         bigm, None,
                                                         constraint_name)
                if not need_lower and not need_upper:
                    return lower, upper
        return lower, upper

    def _estimate_M(self, expr, name):
        # If there are fixed variables here, unfix them for this calculation,
        # and we'll restore them at the end.
        fixed_vars = ComponentMap()
        if not self.assume_fixed_vars_permanent:
            for v in EXPR.identify_variables(expr, include_fixed=True):
                if v.fixed:
                    fixed_vars[v] = value(v)
                    v.fixed = False

        # Calculate a best guess at M
        repn = generate_standard_repn(expr, quadratic=False)
        M = [0, 0]

        if not repn.is_nonlinear():
            if repn.constant is not None:
                for i in (0, 1):
                    if M[i] is not None:
                        M[i] += repn.constant

            for i, coef in enumerate(repn.linear_coefs or []):
                var = repn.linear_vars[i]
                bounds = (value(var.lb), value(var.ub))
                for i in (0, 1):
                    # reverse the bounds if the coefficient is negative
                    if coef > 0:
                        j = i
                    else:
                        j = 1 - i

                    if bounds[i] is not None:
                        M[j] += value(bounds[i]) * coef
                    else:
                        raise GDP_Error(
                            "Cannot estimate M for "
                            "expressions with unbounded variables."
                            "\n\t(found unbounded var '%s' while processing "
                            "constraint '%s')" % (var.name, name))
        else:
            # expression is nonlinear. Try using `contrib.fbbt` to estimate.
            expr_lb, expr_ub = compute_bounds_on_expr(expr)
            if expr_lb is None or expr_ub is None:
                raise GDP_Error("Cannot estimate M for unbounded nonlinear "
                                "expressions.\n\t(found while processing "
                                "constraint '%s')" % name)
            else:
                M = (expr_lb, expr_ub)

        # clean up if we unfixed things (fixed_vars is empty if we were assuming
        # fixed vars are fixed for life)
        for v, val in fixed_vars.items():
            v.fix(val)

        return tuple(M)

    # These are all functions to retrieve transformed components from
    # original ones and vice versa.

    @wraps(get_src_disjunct)
    def get_src_disjunct(self, transBlock):
        return get_src_disjunct(transBlock)

    @wraps(get_src_disjunction)
    def get_src_disjunction(self, xor_constraint):
        return get_src_disjunction(xor_constraint)

    @wraps(get_src_constraint)
    def get_src_constraint(self, transformedConstraint):
        return get_src_constraint(transformedConstraint)

    @wraps(get_transformed_constraints)
    def get_transformed_constraints(self, srcConstraint):
        return get_transformed_constraints(srcConstraint)

    @deprecated("The get_m_value_src function is deprecated. Use "
                "the get_M_value_src function is you need source "
                "information or the get_M_value function if you "
                "only need values.", version='5.7.1')
    def get_m_value_src(self, constraint):
        transBlock = _get_constraint_transBlock(constraint)
        ((lower_val, lower_source, lower_key),
         (upper_val, upper_source, upper_key)) = transBlock.bigm_src[constraint]
        
        if constraint.lower is not None and constraint.upper is not None and \
           (not lower_source is upper_source or not lower_key is upper_key):
            raise GDP_Error("This is why this method is deprecated: The lower "
                            "and upper M values for constraint %s came from "
                            "different sources, please use the get_M_value_src "
                            "method." % constraint.name)
        # if source and key are equal for the two, this is representable in the
        # old format.
        if constraint.lower is not None and lower_source is not None:
            return (lower_source, lower_key)
        if constraint.upper is not None and upper_source is not None:
            return (upper_source, upper_key)
        # else it was calculated:
        return (lower_val, upper_val)

    def get_M_value_src(self, constraint):
        """Return a tuple indicating how the M value used to transform
        constraint was specified. (In particular, this can be used to
        verify which BigM Suffixes were actually necessary to the
        transformation.)

        Return is of the form: ((lower_M_val, lower_M_source, lower_M_key),
                                (upper_M_val, upper_M_source, upper_M_key))

        If the constraint does not have a lower bound (or an upper bound), 
        the first (second) element will be (None, None, None). Note that if
        a constraint is of the form a <= expr <= b or is an equality constraint,
        it is not necessarily true that the source of lower_M and upper_M
        are the same.

        If the M value came from an arg, source is the  dictionary itself and 
        key is the key in that dictionary which gave us the M value.

        If the M value came from a Suffix, source is the BigM suffix used and 
        key is the key in that Suffix.

        If the transformation calculated the value, both source and key are None.

        Parameters
        ----------
        constraint: Constraint, which must be in the subtree of a transformed
                    Disjunct
        """
        transBlock = _get_constraint_transBlock(constraint)
        # This is a KeyError if it fails, but it is also my fault if it
        # fails... (That is, it's a bug in the mapping.)
        return transBlock.bigm_src[constraint]

    def get_M_value(self, constraint):
        """Returns the M values used to transform constraint. Return is a tuple:
        (lower_M_value, upper_M_value). Either can be None if constraint does 
        not have a lower or upper bound, respectively.

        Parameters
        ----------
        constraint: Constraint, which must be in the subtree of a transformed
                    Disjunct
        """
        transBlock = _get_constraint_transBlock(constraint)
        # This is a KeyError if it fails, but it is also my fault if it
        # fails... (That is, it's a bug in the mapping.)
        lower, upper = transBlock.bigm_src[constraint]
        return (lower[0], upper[0])
Esempio n. 21
0
class SeparatorData(UnitModelBlockData):
    """
    This is a general purpose model for a Separator block with the IDAES
    modeling framework. This block can be used either as a stand-alone
    Separator unit operation, or as a sub-model within another unit operation.

    This model creates a number of StateBlocks to represent the outgoing
    streams, then writes a set of phase-component material balances, an
    overall enthalpy balance (2 options), and a momentum balance (2 options)
    linked to a mixed-state StateBlock. The mixed-state StateBlock can either
    be specified by the user (allowing use as a sub-model), or created by the
    Separator.

    When being used as a sub-model, Separator should only be used when a
    set of new StateBlocks are required for the streams to be separated. It
    should not be used to separate streams to go to mutiple ControlVolumes in a
    single unit model - in these cases the unit model developer should write
    their own splitting equations.
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([False]),
                    default=False,
                    description="Dynamic model flag - must be False",
                    doc="""Indicates whether this model will be dynamic or not,
**default** = False. Product blocks are always steady-state."""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Product blocks do not contain holdup, thus this must be
False."""))
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for mixer",
            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(
        "outlet_list",
        ConfigValue(domain=list_of_strings,
                    description="List of outlet names",
                    doc="""A list containing names of outlets,
**default** - None.
**Valid values:** {
**None** - use num_outlets argument,
**list** - a list of names to use for outlets.}"""))
    CONFIG.declare(
        "num_outlets",
        ConfigValue(
            domain=int,
            description="Number of outlets to unit",
            doc="""Argument indicating number (int) of outlets to construct, not
used if outlet_list arg is provided,
**default** - None.
**Valid values:** {
**None** - use outlet_list arg instead, or default to 2 if neither argument
provided,
**int** - number of outlets to create (will be named with sequential integers
from 1 to num_outlets).}"""))
    CONFIG.declare(
        "split_basis",
        ConfigValue(
            default=SplittingType.totalFlow,
            domain=SplittingType,
            description="Basis for splitting stream",
            doc="""Argument indicating basis to use for splitting mixed stream,
**default** - SplittingType.totalFlow.
**Valid values:** {
**SplittingType.totalFlow** - split based on total flow (split
fraction indexed only by time and outlet),
**SplittingType.phaseFlow** - split based on phase flows (split fraction
indexed by time, outlet and phase),
**SplittingType.componentFlow** - split based on component flows (split
fraction indexed by time, outlet and components),
**SplittingType.phaseComponentFlow** - split based on phase-component flows (
split fraction indexed by both time, outlet, phase and components).}"""))
    CONFIG.declare(
        "energy_split_basis",
        ConfigValue(
            default=EnergySplittingType.equal_temperature,
            domain=EnergySplittingType,
            description="Type of constraint to write for energy splitting",
            doc="""Argument indicating basis to use for splitting energy this is
not used for when ideal_separation == True.
**default** - EnergySplittingType.equal_temperature.
**Valid values:** {
**EnergySplittingType.equal_temperature** - outlet temperatures equal inlet
**EnergySplittingType.equal_molar_enthalpy** - oulet molar enthalpies equal
inlet}"""))
    CONFIG.declare(
        "ideal_separation",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Ideal splitting flag",
            doc="""Argument indicating whether ideal splitting should be used.
Ideal splitting assumes perfect spearation of material, and attempts to
avoid duplication of StateBlocks by directly partitioning outlet flows to
ports,
**default** - True.
**Valid values:** {
**True** - use ideal splitting methods,
**False** - use explicit splitting equations with split fractions.}"""))
    CONFIG.declare(
        "ideal_split_map",
        ConfigValue(
            domain=dict,
            description="Ideal splitting partitioning map",
            doc="""Dictionary containing information on how extensive variables
should be partitioned when using ideal splitting (ideal_separation = True).
**default** - None.
**Valid values:** {
**dict** with keys of indexing set members and values indicating which outlet
this combination of keys should be partitioned to.
E.g. {("Vap", "H2"): "outlet_1"}}"""))
    CONFIG.declare(
        "mixed_state_block",
        ConfigValue(
            domain=is_state_block,
            description="Existing StateBlock to use as mixed stream",
            doc="""An existing state block to use as the source stream from the
Separator block,
**default** - None.
**Valid values:** {
**None** - create a new StateBlock for the mixed stream,
**StateBlock** - a StateBock to use as the source for the mixed stream.}"""))
    CONFIG.declare(
        "construct_ports",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Construct inlet and outlet Port objects",
            doc=
            """Argument indicating whether model should construct Port objects
linked the mixed state and all outlet states,
**default** - True.
**Valid values:** {
**True** - construct Ports for all states,
**False** - do not construct Ports."""))

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

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

        Args:
            None

        Returns:
            None
        """
        # Call super.build()
        super(SeparatorData, self).build()

        # Call setup methods from ControlVolumeBlockData
        self._get_property_package()
        self._get_indexing_sets()

        # Create list of inlet names
        outlet_list = self.create_outlet_list()

        if self.config.mixed_state_block is None:
            mixed_block = self.add_mixed_state_block()
        else:
            mixed_block = self.get_mixed_state_block()

        # Add inlet port
        self.add_inlet_port_objects(mixed_block)

        # Construct splitter based on ideal_separation argument
        if self.config.ideal_separation:
            # Use ideal partitioning method
            self.partition_outlet_flows(mixed_block, outlet_list)
        else:
            # Otherwise, Build StateBlocks for outlet
            outlet_blocks = self.add_outlet_state_blocks(outlet_list)

            # Add split fractions
            self.add_split_fractions(outlet_list)

            # Construct splitting equations
            self.add_material_splitting_constraints(mixed_block)
            self.add_energy_splitting_constraints(mixed_block)
            self.add_momentum_splitting_constraints(mixed_block)

            # Construct outlet port objects
            self.add_outlet_port_objects(outlet_list, outlet_blocks)

    def create_outlet_list(self):
        """
        Create list of outlet stream names based on config arguments.

        Returns:
            list of strings
        """
        if (self.config.outlet_list is not None
                and self.config.num_outlets is not None):
            # If both arguments provided and not consistent, raise Exception
            if len(self.config.outlet_list) != self.config.num_outlets:
                raise ConfigurationError(
                    "{} Separator provided with both outlet_list and "
                    "num_outlets arguments, which were not consistent ("
                    "length of outlet_list was not equal to num_outlets). "
                    "PLease check your arguments for consistency, and "
                    "note that it is only necessry to provide one of "
                    "these arguments.".format(self.name))
        elif (self.config.outlet_list is None
              and self.config.num_outlets is None):
            # If no arguments provided for outlets, default to num_outlets = 2
            self.config.num_outlets = 2

        # Create a list of names for outlet StateBlocks
        if self.config.outlet_list is not None:
            outlet_list = self.config.outlet_list
        else:
            outlet_list = [
                'outlet_' + str(n)
                for n in range(1, self.config.num_outlets + 1)
            ]

        return outlet_list

    def add_outlet_state_blocks(self, outlet_list):
        """
        Construct StateBlocks for all outlet streams.

        Args:
            list of strings to use as StateBlock names

        Returns:
            list of StateBlocks
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["parameters"] = self.config.property_package
        tmp_dict["defined_state"] = False

        # Create empty list to hold StateBlocks for return
        outlet_blocks = []

        # Create an instance of StateBlock for all outlets
        for o in outlet_list:
            o_obj = self.config.property_package.state_block_class(
                self.flowsheet().config.time,
                doc="Material properties at outlet",
                default=tmp_dict)

            setattr(self, o + "_state", o_obj)

            outlet_blocks.append(getattr(self, o + "_state"))

        return outlet_blocks

    def add_mixed_state_block(self):
        """
        Constructs StateBlock to represent mixed stream.

        Returns:
            New StateBlock object
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["parameters"] = self.config.property_package
        tmp_dict["defined_state"] = True

        self.mixed_state = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            doc="Material properties of mixed stream",
            default=tmp_dict)

        return self.mixed_state

    def get_mixed_state_block(self):
        """
        Validates StateBlock provided in user arguments for mixed stream.

        Returns:
            The user-provided StateBlock or an Exception
        """
        # Sanity check to make sure method is not called when arg missing
        if self.config.mixed_state_block is None:
            raise BurntToast("{} get_mixed_state_block method called when "
                             "mixed_state_block argument is None. This should "
                             "not happen.".format(self.name))

        # Check that the user-provided StateBlock uses the same prop pack
        if (self.config.mixed_state_block[self.flowsheet().config.time.first(
        )].config.parameters != self.config.property_package):
            raise ConfigurationError(
                "{} StateBlock provided in mixed_state_block argument "
                " does not come from the same property package as "
                "provided in the property_package argument. All "
                "StateBlocks within a Separator must use the same "
                "property package.".format(self.name))

        return self.config.mixed_state_block

    def add_inlet_port_objects(self, mixed_block):
        """
        Adds inlet Port object if required.

        Args:
            a mixed state StateBlock object

        Returns:
            None
        """
        if self.config.construct_ports is True:
            self.add_port(name="inlet", block=mixed_block, doc="Inlet Port")

    def add_outlet_port_objects(self, outlet_list, outlet_blocks):
        """
        Adds outlet Port objects if required.

        Args:
            a list of outlet StateBlock objects

        Returns:
            None
        """
        if self.config.construct_ports is True:
            # Add ports
            for p in outlet_list:
                o_state = getattr(self, p + "_state")
                self.add_port(name=p, block=o_state, doc="Outlet Port")

    def add_split_fractions(self, outlet_list):
        """
        Creates outlet Port objects and tries to partiton mixed stream flows
        between these

        Args:
            StateBlock representing the mixed flow to be split
            a list of names for outlets

        Returns:
            None
        """
        self.outlet_idx = Set(initialize=outlet_list)

        if self.config.split_basis == SplittingType.totalFlow:
            sf_idx = [self.flowsheet().config.time, self.outlet_idx]
            sf_sum_idx = [self.flowsheet().config.time]
        elif self.config.split_basis == SplittingType.phaseFlow:
            sf_idx = [
                self.flowsheet().config.time, self.outlet_idx,
                self.config.property_package.phase_list
            ]
            sf_sum_idx = [
                self.flowsheet().config.time,
                self.config.property_package.phase_list
            ]
        elif self.config.split_basis == SplittingType.componentFlow:
            sf_idx = [
                self.flowsheet().config.time, self.outlet_idx,
                self.config.property_package.component_list
            ]
            sf_sum_idx = [
                self.flowsheet().config.time,
                self.config.property_package.component_list
            ]
        elif self.config.split_basis == SplittingType.phaseComponentFlow:
            sf_idx = [
                self.flowsheet().config.time, self.outlet_idx,
                self.config.property_package.phase_list,
                self.config.property_package.component_list
            ]
            sf_sum_idx = [
                self.flowsheet().config.time,
                self.config.property_package.phase_list,
                self.config.property_package.component_list
            ]
        else:
            raise BurntToast("{} split_basis has unexpected value. This "
                             "should not happen.".format(self.name))

        # Create split fraction variable
        self.split_fraction = Var(*sf_idx,
                                  initialize=0.5,
                                  doc="Outlet split fractions")

        # Add constraint that split fractions sum to 1
        def sum_sf_rule(b, t, *args):
            return 1 == sum(b.split_fraction[t, o, args]
                            for o in self.outlet_idx)

        self.sum_split_frac = Constraint(*sf_sum_idx, rule=sum_sf_rule)

    def add_material_splitting_constraints(self, mixed_block):
        """
        Creates constraints for splitting the material flows
        """
        def sf(t, o, p, j):
            if self.config.split_basis == SplittingType.totalFlow:
                return self.split_fraction[t, o]
            elif self.config.split_basis == SplittingType.phaseFlow:
                return self.split_fraction[t, o, p]
            elif self.config.split_basis == SplittingType.componentFlow:
                return self.split_fraction[t, o, j]
            elif self.config.split_basis == SplittingType.phaseComponentFlow:
                return self.split_fraction[t, o, p, j]

        @self.Constraint(self.flowsheet().config.time,
                         self.outlet_idx,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Material splitting equations")
        def material_splitting_eqn(b, t, o, p, j):
            o_block = getattr(self, o + "_state")
            return (sf(t, o, p, j) * mixed_block[t].get_material_flow_terms(
                p, j) == o_block[t].get_material_flow_terms(p, j))

    def add_energy_splitting_constraints(self, mixed_block):
        """
        Creates constraints for splitting the energy flows - done by equating
        temperatures in outlets.
        """
        if self.config.energy_split_basis == \
                EnergySplittingType.equal_temperature:

            @self.Constraint(self.flowsheet().config.time,
                             self.outlet_idx,
                             doc="Temperature equality constraint")
            def temperature_equality_eqn(b, t, o):
                o_block = getattr(self, o + "_state")
                return mixed_block[t].temperature == o_block[t].temperature
        elif self.config.energy_split_basis == \
                EnergySplittingType.equal_molar_enthalpy:

            @self.Constraint(self.flowsheet().config.time,
                             self.outlet_idx,
                             doc="Molar enthalpy equality constraint")
            def molar_enthalpy_equality_eqn(b, t, o):
                o_block = getattr(self, o + "_state")
                return mixed_block[t].enth_mol == o_block[t].enth_mol

    def add_momentum_splitting_constraints(self, mixed_block):
        """
        Creates constraints for splitting the momentum flows - done by equating
        pressures in outlets.
        """
        @self.Constraint(self.flowsheet().config.time,
                         self.outlet_idx,
                         doc="Pressure equality constraint")
        def pressure_equality_eqn(b, t, o):
            o_block = getattr(self, o + "_state")
            return mixed_block[t].pressure == o_block[t].pressure

    def partition_outlet_flows(self, mb, outlet_list):
        """
        Creates outlet Port objects and tries to partiton mixed stream flows
        between these

        Args:
            StateBlock representing the mixed flow to be split
            a list of names for outlets

        Returns:
            None
        """
        # Check arguments
        if self.config.construct_ports is False:
            raise ConfigurationError("{} cannot have and ideal separator "
                                     "(ideal_separation = True) with "
                                     "construct_ports = False.".format(
                                         self.name))
        if self.config.split_basis == SplittingType.totalFlow:
            raise ConfigurationError("{} cannot do an ideal separation based "
                                     "on total flow.".format(self.name))
        if self.config.ideal_split_map is None:
            raise ConfigurationError(
                "{} was not provided with an "
                "ideal_split_map argument which is "
                "necessary for doing an ideal_separation.".format(self.name))

        # Validate split map
        split_map = self.config.ideal_split_map
        idx_list = []
        if self.config.split_basis == SplittingType.phaseFlow:
            for p in self.config.property_package.phase_list:
                idx_list.append((p))

            if len(idx_list) != len(split_map):
                raise ConfigurationError(
                    "{} ideal_split_map does not match with "
                    "split_basis chosen. ideal_split_map must"
                    " have a key for each combination of indices.".format(
                        self.name))
            for k in idx_list:
                if k not in split_map:
                    raise ConfigurationError(
                        "{} ideal_split_map does not match with "
                        "split_basis chosen. ideal_split_map must"
                        " have a key for each combination of indices.".format(
                            self.name))

        elif self.config.split_basis == SplittingType.componentFlow:
            for j in self.config.property_package.component_list:
                idx_list.append((j))

            if len(idx_list) != len(split_map):
                raise ConfigurationError(
                    "{} ideal_split_map does not match with "
                    "split_basis chosen. ideal_split_map must"
                    " have a key for each component.".format(self.name))
        elif self.config.split_basis == SplittingType.phaseComponentFlow:
            for p in self.config.property_package.phase_list:
                for j in self.config.property_package.component_list:
                    idx_list.append((p, j))

            if len(idx_list) != len(split_map):
                raise ConfigurationError(
                    "{} ideal_split_map does not match with "
                    "split_basis chosen. ideal_split_map must"
                    " have a key for each phase-component pair.".format(
                        self.name))

        # Check that no. outlets matches split_basis
        if len(outlet_list) != len(idx_list):
            raise ConfigurationError(
                "{} Cannot perform ideal separation. Must have one "
                "outlet for each possible combination of the "
                "chosen split_basis.".format(self.name))

        # Create tolerance Parameter for 0 flow outlets
        self.eps = Param(default=1e-8, mutable=True)

        # Get list of port members
        s_vars = mb[self.flowsheet().config.time.first()].define_port_members()

        # Add empty Port objects
        for o in outlet_list:
            p_obj = Port(noruleinit=True, doc="Outlet Port")
            setattr(self, o, p_obj)

            # Iterate over members to create References or Expressions
            for s in s_vars:
                # Get local variable name of component
                l_name = s_vars[s].local_name

                if l_name == "pressure" or l_name == "temperature":
                    # Assume outlets same as mixed flow - make Reference
                    e_obj = Reference(mb[:].component(l_name))

                elif (l_name.startswith("mole_frac")
                      or l_name.startswith("mass_frac")):
                    # Mole and mass frac need special handling
                    if l_name.endswith("_phase"):

                        def e_rule(b, t, p, j):
                            if self.config.split_basis == \
                                        SplittingType.phaseFlow:
                                s_check = split_map[p]
                            elif self.config.split_basis == \
                                    SplittingType.componentFlow:
                                s_check = split_map[j]
                            elif self.config.split_basis == \
                                    SplittingType.phaseComponentFlow:
                                s_check = split_map[p, j]
                            else:
                                raise BurntToast(
                                    "{} This should not happen. Please "
                                    "report this bug to the IDAES "
                                    "developers.".format(self.name))

                            if s_check == o:
                                return mb[t].component(l_name)[p, j]
                            else:
                                return self.eps

                        e_obj = Expression(
                            self.flowsheet().config.time,
                            self.config.property_package.phase_list,
                            self.config.property_package.component_list,
                            rule=e_rule)

                    else:
                        if self.config.split_basis == \
                                    SplittingType.componentFlow:

                            def e_rule(b, t, j):
                                if split_map[j] == o:
                                    return mb[t].component(l_name)[j]
                                # else:
                                return self.eps

                        else:

                            def e_rule(b, t, j):
                                try:
                                    mfp = mb[t].component(l_name + "_phase")
                                except AttributeError:
                                    raise AttributeError(
                                        "{} Cannot use ideal splitting with "
                                        "this property package. Package uses "
                                        "indexed port member {} which does not"
                                        " have the correct indexing sets, and "
                                        "an equivalent variable with correct "
                                        "indexing sets is not available.".
                                        format(self.name, s))

                                for p in self.config.property_package.phase_list:
                                    if self.config.split_basis == \
                                            SplittingType.phaseFlow:
                                        s_check = split_map[p]
                                    elif self.config.split_basis == \
                                            SplittingType.phaseComponentFlow:
                                        s_check = split_map[p, j]
                                    else:
                                        raise BurntToast(
                                            "{} This should not happen. Please"
                                            " report this bug to the IDAES "
                                            "developers.".format(self.name))

                                    if s_check == o:
                                        return mfp[p, j]
                                # else:
                                return self.eps

                        e_obj = Expression(
                            self.flowsheet().config.time,
                            self.config.property_package.component_list,
                            rule=e_rule)

                elif l_name.endswith("_phase_comp"):

                    def e_rule(b, t, p, j):
                        if self.config.split_basis == \
                                SplittingType.phaseFlow:
                            s_check = split_map[p]
                        elif self.config.split_basis == \
                                SplittingType.componentFlow:
                            s_check = split_map[j]
                        elif self.config.split_basis == \
                                SplittingType.phaseComponentFlow:
                            s_check = split_map[p, j]
                        else:
                            raise BurntToast(
                                "{} This should not happen. Please"
                                " report this bug to the IDAES "
                                "developers.".format(self.name))

                        if s_check == o:
                            return mb[t].component(l_name)[p, j]
                        else:
                            return self.eps

                    e_obj = Expression(
                        self.flowsheet().config.time,
                        self.config.property_package.phase_list,
                        self.config.property_package.component_list,
                        rule=e_rule)

                elif l_name.endswith("_phase"):
                    if self.config.split_basis == \
                                    SplittingType.phaseFlow:

                        def e_rule(b, t, p):
                            if split_map[p] == o:
                                return mb[t].component(l_name)[p]
                            else:
                                return self.eps

                    else:

                        def e_rule(b, t, p):
                            try:
                                mfp = mb[t].component(l_name + "_comp")
                            except AttributeError:
                                raise AttributeError(
                                    "{} Cannot use ideal splitting with this "
                                    "property package. Package uses indexed "
                                    "port member {} which does not have the "
                                    "correct indexing sets, and an equivalent "
                                    "variable with correct indexing sets is "
                                    "not available.".format(self.name, s))

                            for j in self.config.property_package.component_list:
                                if self.config.split_basis == \
                                        SplittingType.componentFlow:
                                    s_check = split_map[j]
                                elif self.config.split_basis == \
                                        SplittingType.phaseComponentFlow:
                                    s_check = split_map[p, j]
                                else:
                                    raise BurntToast(
                                        "{} This should not happen. Please"
                                        " report this bug to the IDAES "
                                        "developers.".format(self.name))

                                if s_check == o:
                                    return mfp[p, j]
                            # else:
                            return self.eps

                    e_obj = Expression(self.flowsheet().config.time,
                                       self.config.property_package.phase_list,
                                       rule=e_rule)

                elif l_name.endswith("_comp"):
                    if self.config.split_basis == \
                            SplittingType.componentFlow:

                        def e_rule(b, t, j):
                            if split_map[j] == o:
                                return mb[t].component(l_name)[j]
                            else:
                                return self.eps

                    elif self.config.split_basis == \
                            SplittingType.phaseFlow:

                        def e_rule(b, t, j):
                            try:
                                mfp = mb[t].component("{0}_phase{1}".format(
                                    l_name[:-5], s[-5:]))
                            except AttributeError:
                                raise AttributeError(
                                    "{} Cannot use ideal splitting with this "
                                    "property package. Package uses indexed "
                                    "port member {} which does not have the "
                                    "correct indexing sets, and an equivalent "
                                    "variable with correct indexing sets is "
                                    "not available.".format(self.name, s))

                            for p in self.config.property_package.phase_list:
                                if self.config.split_basis == \
                                        SplittingType.phaseFlow:
                                    s_check = split_map[p]
                                elif self.config.split_basis == \
                                        SplittingType.phaseComponentFlow:
                                    s_check = split_map[p, j]
                                else:
                                    raise BurntToast(
                                        "{} This should not happen. Please"
                                        " report this bug to the IDAES "
                                        "developers.".format(self.name))

                                if s_check == o:
                                    return mfp[p, j]
                            # else:
                            return self.eps

                    e_obj = Expression(
                        self.flowsheet().config.time,
                        self.config.property_package.component_list,
                        rule=e_rule)

                else:
                    # Not a recognised state, check for indexing sets
                    if mb[self.flowsheet().config.time.first()].component(
                            l_name).is_indexed():
                        # Is indexed, assume indexes match and partition

                        def e_rule(b, t, k):
                            if split_map[k] == o:
                                try:
                                    return mb[t].component(l_name)[k]
                                except KeyError:
                                    raise KeyError(
                                        "{} Cannot use ideal splitting with"
                                        " this property package. Package uses "
                                        "indexed port member {} which does not"
                                        " have suitable indexing set(s).".
                                        format(self.name, s))
                            else:
                                return self.eps

                        # TODO : Reusing indexing set from first port member.
                        # TODO : Not sure how good of an idea this is.
                        e_obj = Expression(
                            self.flowsheet().config.time,
                            mb[self.flowsheet().config.time.first()].component(
                                l_name).index_set(),
                            rule=e_rule)

                    else:
                        # Is not indexed, look for indexed equivalent
                        try:
                            if self.config.split_basis == \
                                    SplittingType.phaseFlow:

                                def e_rule(b, t):
                                    for p in self.config.property_package.phase_list:
                                        if split_map[p] == o:
                                            return mb[t].component(l_name +
                                                                   "_phase")[p]
                                    # else
                                    return self.eps

                            elif self.config.split_basis == \
                                    SplittingType.componentFlow:

                                def e_rule(b, t):
                                    for j in self.config.property_package.component_list:
                                        if split_map[j] == o:
                                            return mb[t].component(l_name +
                                                                   "_comp")[j]
                                    # else
                                    return self.eps

                            elif self.config.split_basis == \
                                    SplittingType.phaseComponentFlow:

                                def e_rule(b, t):
                                    for p in self.config.property_package.phase_list:
                                        for j in self.config.property_package.component_list:
                                            if split_map[p, j] == o:
                                                return (mb[t].component(
                                                    l_name + "_phase_comp")[p,
                                                                            j])
                                    # else
                                    return self.eps

                        except AttributeError:
                            raise AttributeError(
                                "{} Cannot use ideal splitting with this "
                                "property package. Package uses unindexed "
                                "port member {} which does not have an "
                                "equivalent indexed form.".format(
                                    self.name, s))

                    e_obj = Expression(self.flowsheet().config.time,
                                       rule=e_rule)

                # Add Reference/Expression object to Separator model object
                setattr(self, "_" + o + "_" + l_name + "_ref", e_obj)

                # Add member to Port object
                p_obj.add(e_obj, s)

    def model_check(blk):
        """
        This method executes the model_check methods on the associated state
        blocks (if they exist). This method is generally called by a unit model
        as part of the unit's model_check method.

        Args:
            None

        Returns:
            None
        """
        # Try property block model check
        for t in blk.flowsheet().config.time:
            try:
                if blk.config.mixed_state_block is None:
                    blk.mixed_state[t].model_check()
                else:
                    blk.config.mixed_state_block.model_check()
            except AttributeError:
                _log.warning('{} Separator inlet state block has no '
                             'model check. To correct this, add a '
                             'model_check method to the associated '
                             'StateBlock class.'.format(blk.name))

            try:
                outlet_list = blk.create_outlet_list()
                for o in outlet_list:
                    o_block = getattr(blk, o + "_state")
                    o_block[t].model_check()
            except AttributeError:
                _log.warning(
                    '{} Separator outlet state block has no '
                    'model checks. To correct this, add a model_check'
                    ' method to the associated StateBlock class.'.format(
                        blk.name))

    def initialize(blk, outlvl=0, optarg={}, solver='ipopt', hold_state=False):
        '''
        Initialisation routine for separator (default solver ipopt)

        Keyword Arguments:
            outlvl : sets output level of initialisation routine. **Valid
                     values:** **0** - no output (default), **1** - return
                     solver state for each step in routine, **2** - include
                     solver output infomation (tee=True)
            optarg : solver options dictionary object (default=None)
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')
            hold_state : flag indicating whether the initialization routine
                     should unfix any state variables fixed during
                     initialization, **default** - False. **Valid values:**
                     **True** - states variables are not unfixed, and a dict of
                     returned containing flags for which states were fixed
                     during initialization, **False** - state variables are
                     unfixed after initialization by calling the release_state
                     method.

        Returns:
            If hold_states is True, returns a dict containing flags for which
            states were fixed during initialization.
        '''
        # Set solver options
        if outlvl > 1:
            stee = True
        else:
            stee = False

        opt = SolverFactory(solver)
        opt.options = optarg

        # Initialize mixed state block
        if blk.config.mixed_state_block is not None:
            mblock = blk.config.mixed_state_block
        else:
            mblock = blk.mixed_state
        flags = mblock.initialize(outlvl=outlvl - 1,
                                  optarg=optarg,
                                  solver=solver,
                                  hold_state=True)

        if blk.config.ideal_separation:
            # If using ideal splitting, initialisation should be complete
            return flags

        # Initialize outlet StateBlocks
        outlet_list = blk.create_outlet_list()

        for o in outlet_list:
            # Get corresponding outlet StateBlock
            o_block = getattr(blk, o + "_state")

            for t in blk.flowsheet().config.time:

                # Calculate values for state variables
                s_vars = o_block[t].define_state_vars()

                for v in s_vars:
                    m_var = getattr(mblock[t], s_vars[v].local_name)

                    if "flow" in v:
                        # If a "flow" variable, is extensive
                        # Apply split fraction
                        try:
                            for k in s_vars[v]:
                                if (k is None or blk.config.split_basis
                                        == SplittingType.totalFlow):
                                    s_vars[v][k].value = value(
                                        m_var[k] * blk.split_fraction[(t, o)])
                                else:
                                    s_vars[v][k].value = value(
                                        m_var[k] *
                                        blk.split_fraction[(t, o) + k])
                        except KeyError:
                            raise KeyError(
                                "{} state variable and split fraction "
                                "indexing sets do not match. The in-built"
                                " initialization routine for Separators "
                                "relies on the split fraction and state "
                                "variable indexing sets matching to "
                                "calculate initial guesses for extensive "
                                "variables. In other cases users will "
                                "need to provide their own initial "
                                "guesses".format(blk.name))
                    else:
                        # Otherwise intensive, equate to mixed stream
                        for k in s_vars[v]:
                            s_vars[v][k].value = m_var[k].value

                # Call initialization routine for outlet StateBlock
                o_block.initialize(outlvl=outlvl - 1,
                                   optarg=optarg,
                                   solver=solver,
                                   hold_state=False)

        if blk.config.mixed_state_block is None:
            results = opt.solve(blk, tee=stee)

            if outlvl > 0:
                if results.solver.termination_condition == \
                        TerminationCondition.optimal:
                    _log.info('{} Initialisation Complete.'.format(blk.name))
                else:
                    _log.warning('{} Initialisation Failed.'.format(blk.name))
        else:
            _log.info('{} Initialisation Complete.'.format(blk.name))

        if hold_state is True:
            return flags
        else:
            blk.release_state(flags, outlvl=outlvl - 1)

    def release_state(blk, flags, outlvl=0):
        '''
        Method to release state variables fixed during initialisation.

        Keyword Arguments:
            flags : dict containing information of which state variables
                    were fixed during initialization, and should now be
                    unfixed. This dict is returned by initialize if
                    hold_state = True.
            outlvl : sets output level of logging

        Returns:
            None
        '''
        if blk.config.mixed_state_block is None:
            mblock = blk.mixed_state
        else:
            mblock = blk.config.mixed_state_block

        mblock.release_state(flags, outlvl=outlvl - 1)
Esempio n. 22
0
class TranslatorData(UnitModelBlockData):
    """
    Standard Translator Block Class
    """

    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc="""Translator blocks are always steady-state.""",
        ),
    )
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Translator blocks do not contain holdup.""",
        ),
    )
    CONFIG.declare(
        "outlet_state_defined",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Indicated whether outlet state will be fully defined",
            doc="""Indicates whether unit model will fully define outlet state.
If False, the outlet property package will enforce constraints such as sum
of mole fractions and phase equilibrium.
**default** - True.
**Valid values:** {
**True** - outlet state will be fully defined,
**False** - outlet property package should enforce sumation and equilibrium
constraints.}""",
        ),
    )
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Indicates whether outlet is in phase equilibrium",
            doc="""Indicates whether outlet property package should enforce
phase equilibrium constraints.
**default** - False.
**Valid values:** {
**True** - outlet property package should calculate phase equilibrium,
**False** - outlet property package should notcalculate phase equilibrium.}
""",
        ),
    )
    CONFIG.declare(
        "inlet_property_package",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Property package to use for incoming stream",
            doc="""Property parameter object used to define property
calculations for the incoming stream,
**default** - None.
**Valid values:** {
**PhysicalParameterObject** - a PhysicalParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "inlet_property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property package "
            "of the incoming stream",
            doc="""A ConfigBlock with arguments to be passed to the property
block associated with the incoming stream,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
        ),
    )
    CONFIG.declare(
        "outlet_property_package",
        ConfigValue(
            default=None,
            domain=is_physical_parameter_block,
            description="Property package to use for outgoing stream",
            doc="""Property parameter object used to define property
calculations for the outgoing stream,
**default** - None.
**Valid values:** {
**PhysicalParameterObject** - a PhysicalParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "outlet_property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property package "
            "of the outgoing stream",
            doc="""A ConfigBlock with arguments to be passed to the property
block associated with the outgoing stream,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
        ),
    )

    def build(self):
        """
        Begin building model.

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(TranslatorData, self).build()

        # Check construction argumnet consistency
        if (self.config.outlet_state_defined
                and self.config.has_phase_equilibrium):
            raise ConfigurationError(
                "{} cannot calcuate phase equilibrium (has_phase_equilibrium "
                "= True) when outlet state is set to be fully defined ("
                "outlet_state_defined = True).".format(self.name))

        # Add State Blocks
        self.properties_in = self.config.inlet_property_package.build_state_block(
            self.flowsheet().config.time,
            doc="Material properties in incoming stream",
            default={
                "defined_state": True,
                "has_phase_equilibrium": False,
                **self.config.inlet_property_package_args,
            },
        )

        self.properties_out = self.config.outlet_property_package.build_state_block(
            self.flowsheet().config.time,
            doc="Material properties in outgoing stream",
            default={
                "defined_state": self.config.outlet_state_defined,
                "has_phase_equilibrium": self.config.has_phase_equilibrium,
                **self.config.outlet_property_package_args,
            },
        )

        # Add outlet port
        self.add_port(name="inlet", block=self.properties_in, doc="Inlet Port")
        self.add_port(name="outlet",
                      block=self.properties_out,
                      doc="Outlet Port")

    def initialize(
        blk,
        state_args_in=None,
        state_args_out=None,
        outlvl=idaeslog.NOTSET,
        solver=None,
        optarg=None,
    ):
        """
        This method calls the initialization method of the state blocks.

        Keyword Arguments:
            state_args_in : a dict of arguments to be passed to the inlet
                            property package (to provide an initial state for
                            initialization (see documentation of the specific
                            property package) (default = None).
            state_args_out : a dict of arguments to be passed to the outlet
                             property package (to provide an initial state for
                             initialization (see documentation of the specific
                             property package) (default = None).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None, use
                     default solver options)
            solver : str indicating which solver to use during
                     initialization (default = None, use default solver)

        Returns:
            None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        # ---------------------------------------------------------------------
        # Initialize state block
        flags = blk.properties_in.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args_in,
            hold_state=True,
        )

        blk.properties_out.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args_out,
        )

        if degrees_of_freedom(blk) == 0:
            with idaeslog.solver_log(init_log, idaeslog.DEBUG) as slc:
                res = opt.solve(blk, tee=slc.tee)

            init_log.info("Initialization Complete {}.".format(
                idaeslog.condition(res)))
        else:
            init_log.warning("Initialization incomplete. Degrees of freedom "
                             "were not zero. Please provide sufficient number "
                             "of constraints linking the state variables "
                             "between the two state blocks.")

        blk.properties_in.release_state(flags=flags, outlvl=outlvl)
Esempio n. 23
0
class WaterFlashData(UnitModelBlockData):
    """
Simplified Flash Unit Model Class, only for IAPWS with mixed state
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(default=False,
                    domain=In([False]),
                    description="Dynamic model flag",
                    doc="""Indicates whether the model is dynamic"""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([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.}"""))

    def build(self):
        """
        Args:
            None

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

        self.mixed_state = self.config.property_package.build_state_block(
            self.flowsheet().time, default=self.config.property_package_args)

        self.add_port("inlet", self.mixed_state)

        self.vap_state = self.config.property_package.build_state_block(
            self.flowsheet().time, default=self.config.property_package_args)

        self.liq_state = self.config.property_package.build_state_block(
            self.flowsheet().time, default=self.config.property_package_args)

        self.add_port("vap_outlet", self.vap_state)
        self.add_port("liq_outlet", self.liq_state)
        # vapor outlet state
        @self.Constraint(self.flowsheet().time)
        def vap_material_balance(b, t):
            return 1e-4*b.mixed_state[t].flow_mol*b.mixed_state[t].vapor_frac == \
                b.vap_state[t].flow_mol*1e-4

        @self.Constraint(self.flowsheet().time)
        def vap_enthalpy_balance(b, t):
            return b.mixed_state[t].enth_mol_phase["Vap"]*1e-4 == \
                b.vap_state[t].enth_mol*1e-4

        @self.Constraint(self.flowsheet().time)
        def vap_pressure_balance(b, t):
            return b.mixed_state[t].pressure*1e-6 == \
                b.vap_state[t].pressure*1e-6

        # liquid outlet state
        @self.Constraint(self.flowsheet().time)
        def liq_material_balance(b, t):
            return 1e-4*b.mixed_state[t].flow_mol*(1 - b.mixed_state[t].vapor_frac)\
                == b.liq_state[t].flow_mol*1e-4

        @self.Constraint(self.flowsheet().time)
        def liq_enthalpy_balance(b, t):
            return 1e-4*b.mixed_state[t].enth_mol_phase["Liq"] == \
                b.liq_state[t].enth_mol*1e-4

        @self.Constraint(self.flowsheet().time)
        def liq_pressure_balance(b, t):
            return b.mixed_state[t].pressure*1e-6 == \
                b.liq_state[t].pressure*1e-6

    def initialize(blk,
                   state_args_water_steam=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        '''
        Drum initialization routine.

        Keyword Arguments:
            state_args_water_steam : a dict of arguments to be passed to the
                           property package(s) for the control_volume of the
                           model to provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            outlvl : sets output level of initialisation routine
            optarg : solver options dictionary object (default=None, use
                     default solver options)
            solver : str indicating which solver to use during
                     initialization (default = None, use default solver)

        Returns:
            None
        '''
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")

        init_log.info_low("Starting initialization...")
        # fix FeedWater Inlet
        flags_fw = fix_state_vars(blk.mixed_state, state_args_water_steam)
        blk.mixed_state.initialize(solver=solver, optarg=optarg, outlvl=outlvl)
        # initialize outlet states
        for t in blk.flowsheet().time:
            blk.vap_state[t].flow_mol = value(blk.mixed_state[t].flow_mol *
                                              blk.mixed_state[t].vapor_frac)
            blk.vap_state[t].enth_mol = value(
                blk.mixed_state[t].enth_mol_phase["Vap"])
            blk.vap_state[t].pressure = value(blk.mixed_state[t].pressure)
            blk.vap_state[t].vapor_frac = 1
            blk.liq_state[t].flow_mol = value(
                blk.mixed_state[t].flow_mol *
                (1 - blk.mixed_state[t].vapor_frac))
            blk.liq_state[t].enth_mol = value(
                blk.mixed_state[t].enth_mol_phase["Liq"])
            blk.liq_state[t].pressure = value(blk.mixed_state[t].pressure)
            blk.liq_state[t].vapor_frac = 0
        # unfix variables
        revert_state_vars(blk.mixed_state, flags_fw)
        init_log.info_low("Initialization Complete.")

    def set_initial_condition(self):
        pass

    def calculate_scaling_factors(self):
        pass
Esempio n. 24
0
class HelmSplitterData(UnitModelBlockData):
    """
    This is a basic stream splitter which splits flow into outlet streams based
    on split fractions. This does not do phase seperation, and assumes that you
    are using a Helmholtz EOS propery package with P-H state variables. In
    dynamic mode this uses a pseudo-steady-state model.

    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
        ))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
        ),
    )
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for mixer",
            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(
        "outlet_list",
        ConfigValue(
            domain=list_of_strings,
            description="List of outlet names",
            doc="""A list containing names of outlets,
**default** - None.
**Valid values:** {
**None** - use num_outlets argument,
**list** - a list of names to use for outlets.}""",
        ),
    )
    CONFIG.declare(
        "num_outlets",
        ConfigValue(
            domain=int,
            description="Number of outlets to unit",
            doc="""Argument indicating number (int) of outlets to construct,
not used if outlet_list arg is provided,
**default** - None.
**Valid values:** {
**None** - use outlet_list arg instead, or default to 2 if neither argument
provided,
**int** - number of outlets to create (will be named with sequential integers
from 1 to num_outlets).}""",
        ),
    )

    def build(self):
        """
        Build a splitter.

        Args:
            None

        Returns:
            None
        """
        time = self.flowsheet().config.time
        super().build()

        self._get_property_package()

        self.create_outlet_list()
        self.add_inlet_state_and_port()
        self.add_outlet_state_blocks()
        self.add_outlet_port_objects()

        self.split_fraction = Var(time,
                                  self.outlet_list,
                                  initialize=1.0 / len(self.outlet_list),
                                  doc="Split fractions for outlet streams")

        @self.Constraint(time, doc="Splt constraint")
        def sum_split(b, t):
            return 1 == sum(self.split_fraction[t, o]
                            for o in self.outlet_list)

        @self.Constraint(time, self.outlet_list, doc="Pressure constraint")
        def pressure_eqn(b, t, o):
            o_block = getattr(self, "{}_state".format(o))
            return self.mixed_state[t].pressure == o_block[t].pressure

        @self.Constraint(time, self.outlet_list, doc="Enthalpy constraint")
        def enthalpy_eqn(b, t, o):
            o_block = getattr(self, "{}_state".format(o))
            return self.mixed_state[t].enth_mol == o_block[t].enth_mol

        @self.Constraint(time, self.outlet_list, doc="Flow constraint")
        def flow_eqn(b, t, o):
            o_block = getattr(self, "{}_state".format(o))
            sf = self.split_fraction[t, o]
            return self.mixed_state[t].flow_mol * sf == o_block[t].flow_mol

    def add_inlet_state_and_port(self):
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["defined_state"] = True
        self.mixed_state = self.config.property_package.build_state_block(
            self.flowsheet().config.time,
            doc="Material properties of mixed (inlet) stream",
            default=tmp_dict,
        )
        self.add_port(name="inlet", block=self.mixed_state, doc="Inlet Port")

    def create_outlet_list(self):
        """
        Create list of outlet stream names based on config arguments.

        Returns:
            list of strings
        """
        config = self.config
        if config.outlet_list is not None and config.num_outlets is not None:
            # If both arguments provided and not consistent, raise Exception
            if len(config.outlet_list) != config.num_outlets:
                raise ConfigurationError(
                    "{} Splitter provided with both outlet_list and "
                    "num_outlets arguments, which were not consistent ("
                    "length of outlet_list was not equal to num_outlets). "
                    "Please check your arguments for consistency, and "
                    "note that it is only necessry to provide one of "
                    "these arguments.".format(self.name))
        elif (config.outlet_list is None and config.num_outlets is None):
            # If no arguments provided for outlets, default to num_outlets = 2
            config.num_outlets = 2

        # Create a list of names for outlet StateBlocks
        if config.outlet_list is not None:
            outlet_list = self.config.outlet_list
        else:
            outlet_list = [
                "outlet_{}".format(n) for n in range(1, config.num_outlets + 1)
            ]
        self.outlet_list = outlet_list

    def add_outlet_state_blocks(self):
        """
        Construct StateBlocks for all outlet streams.

        Args:
            None

        Returns:
            list of StateBlocks
        """
        # Setup StateBlock argument dict
        tmp_dict = dict(**self.config.property_package_args)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["defined_state"] = False

        # Create empty list to hold StateBlocks for return
        self.outlet_blocks = {}

        # Create an instance of StateBlock for all outlets
        for o in self.outlet_list:
            o_obj = self.config.property_package.build_state_block(
                self.flowsheet().config.time,
                doc="Material properties at outlet",
                default=tmp_dict,
            )
            setattr(self, o + "_state", o_obj)
            self.outlet_blocks[o] = o_obj

    def add_outlet_port_objects(self):
        """
        Adds outlet Port objects if required.

        Args:
            None

        Returns:
            None
        """
        self.outlet_ports = {}
        for p in self.outlet_list:
            self.add_port(name=p, block=self.outlet_blocks[p], doc="Outlet")
            self.outlet_ports[p] = getattr(self, p)

    def initialize(self, outlvl=idaeslog.NOTSET, optarg=None, solver=None):
        """
        Initialization routine for splitter

        Keyword Arguments:
            outlvl: sets output level of initialization routine
            optarg: solver options dictionary object (default=None, use
                    default solver options)
            solver: str indicating which solver to use during
                     initialization (default = None, use default solver)

        Returns:
            If hold_states is True, returns a dict containing flags for which
            states were fixed during initialization.
        """
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        # sp is what to save to make sure state after init is same as the start
        sp = StoreSpec.value_isfixed_isactive(only_fixed=True)
        istate = to_json(self, return_dict=True, wts=sp)

        # check for fixed outlet flows and use them to calculate fixed split
        # fractions
        for t in self.flowsheet().config.time:
            for o in self.outlet_list:
                if self.outlet_blocks[o][t].flow_mol.fixed:
                    self.split_fraction[t, o].fix(
                        value(self.mixed_state[t] /
                              self.outlet_blocks[o][t].flow_mol))

        # fix or unfix split fractions so n - 1 are fixed
        for t in self.flowsheet().config.time:
            # see how many split fractions are fixed
            n = sum(1 for o in self.outlet_list
                    if self.split_fraction[t, o].fixed)
            # if number of outlets - 1 we're good
            if n == len(self.outlet_list) - 1:
                continue
            # if too mant are fixed un fix the first, generally assume that is
            # the main flow, and is the calculated split fraction
            if n == len(self.outlet_list):
                self.split_fraction[t, self.outlet_list[0]].unfix()
            # if not enough fixed, start fixing from the back until there are
            # are enough
            for o in reversed(self.outlet_list):
                if not self.split_fraction[t, o].fixed:
                    self.split_fraction[t, o].fix()
                    n += 1
                if n == len(self.outlet_list) - 1:
                    break

        # This model is really simple so it should easily solve without much
        # effort to initialize
        self.inlet.fix()
        for o, p in self.outlet_ports.items():
            p.unfix()
        assert degrees_of_freedom(self) == 0
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info("Initialization Complete: {}".format(
            idaeslog.condition(res)))

        from_json(self, sd=istate, wts=sp)

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        for (t, i), c in self.pressure_eqn.items():
            o_block = getattr(self, "{}_state".format(i))
            s = iscale.get_scaling_factor(o_block[t].pressure)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
        for (t, i), c in self.enthalpy_eqn.items():
            o_block = getattr(self, "{}_state".format(i))
            s = iscale.get_scaling_factor(o_block[t].enth_mol)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
        for (t, i), c in self.flow_eqn.items():
            o_block = getattr(self, "{}_state".format(i))
            s = iscale.get_scaling_factor(o_block[t].flow_mol)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
class EquilibriumReactorData(UnitModelBlockData):
    """
    Standard Equilibrium Reactor Unit Model Class
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=In([False]),
                    default=False,
                    description="Dynamic model flag - must be False",
                    doc="""Indicates whether this model will be dynamic or not,
**default** = False. Equilibrium Reactors do not support dynamic behavior."""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Indicates whether holdup terms should be constructed or not.
**default** - False. Equilibrium reactors do not have defined volume, thus
this must be False."""))
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare(
        "has_rate_reactions",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Rate reaction construction flag",
            doc="""Indicates whether terms for rate controlled reactions
should be constructed, along with constraints equating these to zero,
**default** - True.
**Valid values:** {
**True** - include rate reaction terms,
**False** - exclude rate reaction terms.}"""))
    CONFIG.declare(
        "has_equilibrium_reactions",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Equilibrium reaction construction flag",
            doc="""Indicates whether terms for equilibrium controlled reactions
should be constructed,
**default** - True.
**Valid values:** {
**True** - include equilibrium reaction terms,
**False** - exclude equilibrium reaction terms.}"""))
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Phase equilibrium term construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed, **default** - True.
**Valid values:** {
**True** - include phase equilibrium term,
**False** - exclude phase equlibirum terms.}"""))
    CONFIG.declare(
        "has_heat_transfer",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Heat transfer term construction flag",
            doc=
            """Indicates whether terms for heat transfer should be constructed,
**default** - False.
**Valid values:** {
**True** - include heat transfer terms,
**False** - exclude heat transfer terms.}"""))
    CONFIG.declare(
        "has_heat_of_reaction",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Heat of reaction term construction flag",
            doc="""Indicates whether terms for heat of reaction terms should be
constructed,
**default** - False.
**Valid values:** {
**True** - include heat of reaction terms,
**False** - exclude heat of reaction terms.}"""))
    CONFIG.declare(
        "has_pressure_change",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
constructed,
**default** - False.
**Valid values:** {
**True** - include pressure change terms,
**False** - exclude pressure change 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,
**PhysicalParameterObject** - a PhysicalParameterBlock 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.}"""))

    def build(self):
        """
        Begin building model.

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(EquilibriumReactorData, self).build()

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
                "reaction_package": self.config.reaction_package,
                "reaction_package_args": self.config.reaction_package_args
            })

        # No need for control volume geometry

        self.control_volume.add_state_blocks(
            has_phase_equilibrium=self.config.has_phase_equilibrium)

        self.control_volume.add_reaction_blocks(
            has_equilibrium=self.config.has_equilibrium_reactions)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_rate_reactions=self.config.has_rate_reactions,
            has_equilibrium_reactions=self.config.has_equilibrium_reactions,
            has_phase_equilibrium=self.config.has_phase_equilibrium)

        self.control_volume.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_of_reaction=self.config.has_heat_of_reaction,
            has_heat_transfer=self.config.has_heat_transfer)

        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()

        if self.config.has_rate_reactions:
            # Add equilibrium reactor performance equation
            @self.Constraint(self.flowsheet().config.time,
                             self.config.reaction_package.rate_reaction_idx,
                             doc="Rate reaction equilibrium constraint")
            def rate_reaction_constraint(b, t, r):
                # Set kinetic reaction rates to zero
                return b.control_volume.reactions[t].reaction_rate[r] == 0

        # Set references to balance terms at unit level
        if (self.config.has_heat_transfer is True
                and self.config.energy_balance_type != EnergyBalanceType.none):
            add_object_reference(self, "heat_duty", self.control_volume.heat)

        if (self.config.has_pressure_change is True
                and self.config.momentum_balance_type != 'none'):
            add_object_reference(self, "deltaP", self.control_volume.deltaP)

    def _get_performance_contents(self, time_point=0):
        var_dict = {}
        if hasattr(self, "heat_duty"):
            var_dict["Heat Duty"] = self.heat_duty[time_point]
        if hasattr(self, "deltaP"):
            var_dict["Pressure Change"] = self.deltaP[time_point]

        return {"vars": var_dict}
Esempio n. 26
0
class GibbsReactorData(UnitModelBlockData):
    """
    Standard Gibbs Reactor Unit Model Class

    This model assume all possible reactions reach equilibrium such that the
    system partial molar Gibbs free energy is minimized.
    Since some species mole flow rate might be very small,
    the natural log of the species molar flow rate is used.
    Instead of specifying the system Gibbs free energy as an objective
    function, the equations for zero partial derivatives of the grand function
    with Lagrangian multiple terms with repect to product species mole flow
    rates and the multiples are specified as constraints.
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc=
            """Gibbs reactors do not support dynamic models, thus this must be
False."""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag",
            doc="""Gibbs reactors do not have defined volume, thus this must be
False."""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare(
        "has_heat_transfer",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Heat transfer term construction flag",
            doc=
            """Indicates whether terms for heat transfer should be constructed,
**default** - False.
**Valid values:** {
**True** - include heat transfer terms,
**False** - exclude heat transfer terms.}"""))
    CONFIG.declare(
        "has_pressure_change",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
constructed,
**default** - False.
**Valid values:** {
**True** - include pressure change terms,
**False** - exclude pressure change 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.}"""))

    def build(self):
        """
        Begin building model (pre-DAE transformation).

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(GibbsReactorData, self).build()

        # Build Control Volume
        self.control_volume = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args
            })

        self.control_volume.add_state_blocks(has_phase_equilibrium=False)

        self.control_volume.add_total_element_balances()

        self.control_volume.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_transfer=self.config.has_heat_transfer)

        self.control_volume.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()

        # Add performance equations
        # Add Lagrangian multiplier variables
        self.lagrange_mult = Var(self.flowsheet().config.time,
                                 self.config.property_package.element_list,
                                 domain=Reals,
                                 initialize=100,
                                 doc="Lagrangian multipliers")

        # Use Lagrangian multiple method to derive equations for Out_Fi
        # Use RT*lagrange as the Lagrangian multiple such that lagrange is in
        # a similar order of magnitude as log(Yi)

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Gibbs energy minimisation constraint")
        def gibbs_minimization(b, t, p, j):
            # Use natural log of species mole flow to avoid Pyomo solver
            # warnings of reaching infeasible point
            return 0 == (
                b.control_volume.properties_out[t].gibbs_mol_phase_comp[p, j] +
                sum(b.lagrange_mult[t, e] * b.control_volume.properties_out[t].
                    config.parameters.element_comp[j][e]
                    for e in b.config.property_package.element_list))

        # Set references to balance terms at unit level
        if (self.config.has_heat_transfer is True
                and self.config.energy_balance_type != EnergyBalanceType.none):
            add_object_reference(self, "heat_duty", self.control_volume.heat)
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            add_object_reference(self, "deltaP", self.control_volume.deltaP)
Esempio n. 27
0
class GenericReactionParameterData(ReactionParameterBlock):
    """
    General Reaction Parameter Block Class
    """
    CONFIG = ReactionParameterBlock.CONFIG()

    CONFIG.declare(
        "reaction_basis",
        ConfigValue(
            default=MaterialFlowBasis.molar,
            domain=In(MaterialFlowBasis),
            doc="Basis of reactions",
            description="Argument indicating basis of reaction terms. Should be "
            "an instance of a MaterialFlowBasis Enum"))

    CONFIG.declare("rate_reactions",
                   ConfigBlock(implicit=True, implicit_domain=rate_rxn_config))

    CONFIG.declare(
        "equilibrium_reactions",
        ConfigBlock(implicit=True, implicit_domain=equil_rxn_config))

    # Base units of measurement
    CONFIG.declare(
        "base_units",
        ConfigValue(
            default={},
            domain=dict,
            description="Base units for property package",
            doc="Dict containing definition of base units of measurement to use "
            "with property package."))

    # User-defined default scaling factors
    CONFIG.declare(
        "default_scaling_factors",
        ConfigValue(
            domain=dict,
            description="User-defined default scaling factors",
            doc="Dict of user-defined properties and associated default "
            "scaling factors"))

    def build(self):
        '''
        Callable method for Block construction.
        '''
        # Call super.build() to initialize Block
        # In this case we are replicating the super.build to get around a
        # chicken-and-egg problem
        # The super.build tries to validate units, but they have not been set
        # and cannot be set until the config block is created by super.build
        super(ReactionParameterBlock, self).build()
        self.default_scaling_factor = {}

        # Set base units of measurement
        self.get_metadata().add_default_units(self.config.base_units)

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

        # Call configure method to set construction arguments
        self.configure()

        # Build core components
        self._reaction_block_class = GenericReactionBlock

        # Alias associated property package to keep line length down
        ppack = self.config.property_package

        if not hasattr(ppack, "_electrolyte") or not ppack._electrolyte:
            pc_set = ppack._phase_component_set
        elif ppack.config.state_components.name == "true":
            pc_set = ppack.true_phase_component_set
        elif ppack.config.state_components.name == "apparent":
            pc_set = ppack.apparent_phase_component_set
        else:
            raise BurntToast()

        # Construct rate reaction attributes if required
        if len(self.config.rate_reactions) > 0:
            # Construct rate reaction index
            self.rate_reaction_idx = Set(
                initialize=self.config.rate_reactions.keys())

            # Construct rate reaction stoichiometry dict
            self.rate_reaction_stoichiometry = {}
            for r, rxn in self.config.rate_reactions.items():
                for p, j in pc_set:
                    self.rate_reaction_stoichiometry[(r, p, j)] = 0

                if rxn.stoichiometry is None:
                    raise ConfigurationError(
                        "{} rate reaction {} was not provided with a "
                        "stoichiometry configuration argument.".format(
                            self.name, r))
                else:
                    for k, v in rxn.stoichiometry.items():
                        if k[0] not in ppack.phase_list:
                            raise ConfigurationError(
                                "{} stoichiometry for rate reaction {} "
                                "included unrecognised phase {}.".format(
                                    self.name, r, k[0]))
                        if k[1] not in ppack.component_list:
                            raise ConfigurationError(
                                "{} stoichiometry for rate reaction {} "
                                "included unrecognised component {}.".format(
                                    self.name, r, k[1]))
                        self.rate_reaction_stoichiometry[(r, k[0], k[1])] = v

                # Check that a method was provided for the rate form
                if rxn.rate_form is None:
                    _log.debug(
                        "{} rate reaction {} was not provided with a "
                        "rate_form configuration argument. This is suitable "
                        "for processes using stoichiometric reactors, but not "
                        "for those using unit operations which rely on "
                        "reaction rate.".format(self.name, r))

        # Construct equilibrium reaction attributes if required
        if len(self.config.equilibrium_reactions) > 0:
            # Construct equilibrium reaction index
            self.equilibrium_reaction_idx = Set(
                initialize=self.config.equilibrium_reactions.keys())

            # Construct equilibrium reaction stoichiometry dict
            self.equilibrium_reaction_stoichiometry = {}
            for r, rxn in self.config.equilibrium_reactions.items():
                for p, j in pc_set:
                    self.equilibrium_reaction_stoichiometry[(r, p, j)] = 0

                if rxn.stoichiometry is None:
                    raise ConfigurationError(
                        "{} equilibrium reaction {} was not provided with a "
                        "stoichiometry configuration argument.".format(
                            self.name, r))
                else:
                    for k, v in rxn.stoichiometry.items():
                        if k[0] not in ppack.phase_list:
                            raise ConfigurationError(
                                "{} stoichiometry for equilibrium reaction {} "
                                "included unrecognised phase {}.".format(
                                    self.name, r, k[0]))
                        if k[1] not in ppack.component_list:
                            raise ConfigurationError(
                                "{} stoichiometry for equilibrium reaction {} "
                                "included unrecognised component {}.".format(
                                    self.name, r, k[1]))
                        self.equilibrium_reaction_stoichiometry[(r, k[0],
                                                                 k[1])] = v

                # Check that a method was provided for the equilibrium form
                if rxn.equilibrium_form is None:
                    raise ConfigurationError(
                        "{} equilibrium reaction {} was not provided with a "
                        "equilibrium_form configuration argument.".format(
                            self.name, r))

        # Add a master reaction index which includes both types of reactions
        if (len(self.config.rate_reactions) > 0
                and len(self.config.equilibrium_reactions) > 0):
            self.reaction_idx = Set(
                initialize=(self.rate_reaction_idx
                            | self.equilibrium_reaction_idx))
        elif len(self.config.rate_reactions) > 0:
            self.reaction_idx = Set(initialize=self.rate_reaction_idx)
        elif len(self.config.equilibrium_reactions) > 0:
            self.reaction_idx = Set(initialize=self.equilibrium_reaction_idx)
        else:
            raise BurntToast("{} Generic property package failed to construct "
                             "master reaction Set. This should not happen. "
                             "Please contact the IDAES developers with this "
                             "bug".format(self.name))

        # Construct blocks to contain parameters for each reaction
        for r in self.reaction_idx:
            self.add_component("reaction_" + str(r), Block())

        # Build parameters
        if len(self.config.rate_reactions) > 0:
            for r in self.rate_reaction_idx:
                rblock = getattr(self, "reaction_" + r)
                r_config = self.config.rate_reactions[r]

                order_init = {}
                for p, j in pc_set:
                    if "reaction_order" in r_config.parameter_data:
                        try:
                            order_init[p, j] = r_config.parameter_data[
                                "reaction_order"][p, j]
                        except KeyError:
                            order_init[p, j] = 0
                    else:
                        # Assume elementary reaction and use stoichiometry
                        try:
                            if r_config.stoichiometry[p, j] < 0:
                                # These are reactants, but order is -ve stoic
                                order_init[p,
                                           j] = -r_config.stoichiometry[p, j]
                            else:
                                # Anything else is a product, not be included
                                order_init[p, j] = 0
                        except KeyError:
                            order_init[p, j] = 0

                rblock.reaction_order = Var(pc_set,
                                            initialize=order_init,
                                            doc="Reaction order",
                                            units=None)

                for val in self.config.rate_reactions[r].values():
                    try:
                        val.build_parameters(rblock,
                                             self.config.rate_reactions[r])
                    except AttributeError:
                        pass

        if len(self.config.equilibrium_reactions) > 0:
            for r in self.equilibrium_reaction_idx:
                rblock = getattr(self, "reaction_" + r)
                r_config = self.config.equilibrium_reactions[r]

                order_init = {}
                for p, j in pc_set:
                    if "reaction_order" in r_config.parameter_data:
                        try:
                            order_init[p, j] = r_config.parameter_data[
                                "reaction_order"][p, j]
                        except KeyError:
                            order_init[p, j] = 0
                    else:
                        # Assume elementary reaction and use stoichiometry
                        try:
                            # Here we use the stoic. coeff. directly
                            # However, solids should be excluded as they
                            # normally do not appear in the equilibrium
                            # relationship
                            pobj = ppack.get_phase(p)
                            if not pobj.is_solid_phase():
                                order_init[p, j] = r_config.stoichiometry[p, j]
                            else:
                                order_init[p, j] = 0
                        except KeyError:
                            order_init[p, j] = 0

                rblock.reaction_order = Var(pc_set,
                                            initialize=order_init,
                                            doc="Reaction order",
                                            units=None)

                for val in self.config.equilibrium_reactions[r].values():
                    try:
                        val.build_parameters(
                            rblock, self.config.equilibrium_reactions[r])
                    except AttributeError:
                        pass
                    except KeyError as err:
                        # This likely arises from mismatched true and apparent
                        # species sets. Reaction packages must use the same
                        # basis as the associated thermo properties
                        # Raise an exception to inform the user
                        raise PropertyPackageError(
                            "{} KeyError encountered whilst constructing "
                            "reaction parameters. This may be due to "
                            "mismatched state_components between the "
                            "Reaction Package and the associated Physical "
                            "Property Package - Reaction Packages must use the"
                            "same basis (true or apparent species) as the "
                            "Physical Property Package.".format(self.name),
                            err)

        # As a safety check, make sure all Vars in reaction blocks are fixed
        for v in self.component_objects(Var, descend_into=True):
            for i in v:
                if v[i].value is None:
                    raise ConfigurationError(
                        "{} parameter {} was not assigned"
                        " a value. Please check your configuration "
                        "arguments.".format(self.name, v.local_name))
                v[i].fix()

        # Set default scaling factors
        if self.config.default_scaling_factors is not None:
            self.default_scaling_factor.update(
                self.config.default_scaling_factors)
        # Finally, call populate_default_scaling_factors method to fill blanks
        iscale.populate_default_scaling_factors(self)

    def configure(self):
        """
        Placeholder method to allow users to specify config arguments via a
        class. The user class should inherit from this one and implement a
        configure() method which sets the values of the desired config
        arguments.

        Args:
            None

        Returns:
            None
        """
        pass

    def parameters(self):
        """
        Placeholder method to allow users to specify parameters via a
        class. The user class should inherit from this one and implement a
        parameters() method which creates the required components.

        Args:
            None

        Returns:
            None
        """
        pass

    @classmethod
    def define_metadata(cls, obj):
        """Define properties supported and units."""
        obj.add_properties({
            'dh_rxn': {
                'method': '_dh_rxn'
            },
            'k_eq': {
                'method': '_k_eq'
            },
            'log_k_eq': {
                'method': '_log_k_eq'
            },
            'k_rxn': {
                'method': '_k_rxn'
            },
            'reaction_rate': {
                'method': "_reaction_rate"
            }
        })
Esempio n. 28
0
class GDPbbSolver(object):
    """
    A branch and bound-based solver for Generalized Disjunctive Programming (GDP) problems

    The GDPbb solver solves subproblems relaxing certain disjunctions, and
    builds up a tree of potential active disjunctions. By exploring promising
    branches, it eventually results in an optimal configuration of disjunctions.

    Keyword arguments below are specified for the ``solve`` function.

    """
    CONFIG = ConfigBlock("gdpbb")
    CONFIG.declare(
        "solver",
        ConfigValue(default="baron",
                    description="Subproblem solver to use, defaults to baron"))
    CONFIG.declare(
        "solver_args",
        ConfigBlock(
            implicit=True,
            description="Block of keyword arguments to pass to the solver."))
    CONFIG.declare(
        "tee",
        ConfigValue(default=False,
                    domain=bool,
                    description="Flag to stream solver output to console."))
    CONFIG.declare(
        "check_sat",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "When True, GDPBB will check satisfiability via the pyomo.contrib.satsolver interface at each node"
        ))
    CONFIG.declare(
        "logger",
        ConfigValue(
            default='pyomo.contrib.gdpbb',
            description="The logger object or name to use for reporting.",
            domain=a_logger))
    CONFIG.declare(
        "time_limit",
        ConfigValue(
            default=600,
            domain=PositiveInt,
            description="Time limit (seconds, default=600)",
            doc="Seconds allowed until terminated. Note that the time limit can"
            "currently only be enforced between subsolver invocations. You may"
            "need to set subsolver time limits as well."))

    @deprecated("GDPbb has been merged into GDPopt. "
                "You can use the algorithm using GDPopt with strategy='LBB'.",
                logger="pyomo.solvers",
                version='5.6.9')
    def __init__(self, *args, **kwargs):
        super(GDPbbSolver, self).__init__(*args, **kwargs)

    def available(self, exception_flag=True):
        """Check if solver is available.

        TODO: For now, it is always available. However, sub-solvers may not
        always be available, and so this should reflect that possibility.

        """
        return True

    def version(self):
        return __version__

    def solve(self, model, **kwds):
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)
        return SolverFactory('gdpopt').solve(
            model,
            strategy='LBB',
            minlp_solver=config.solver,
            minlp_solver_args=config.solver_args,
            tee=config.tee,
            check_sat=config.check_sat,
            logger=config.logger,
            time_limit=config.time_limit)

        # Validate model to be used with gdpbb
        self.validate_model(model)
        # Set solver as an MINLP
        solve_data = GDPbbSolveData()
        solve_data.timing = Container()
        solve_data.original_model = model
        solve_data.results = SolverResults()

        old_logger_level = config.logger.getEffectiveLevel()
        with time_code(solve_data.timing, 'total', is_main_timer=True), \
                restore_logger_level(config.logger), \
                create_utility_block(model, 'GDPbb_utils', solve_data):
            if config.tee and old_logger_level > logging.INFO:
                # If the logger does not already include INFO, include it.
                config.logger.setLevel(logging.INFO)
            config.logger.info(
                "Starting GDPbb version %s using %s as subsolver" %
                (".".join(map(str, self.version())), config.solver))

            # Setup results
            solve_data.results.solver.name = 'GDPbb - %s' % (str(
                config.solver))
            setup_results_object(solve_data, config)

            # clone original model for root node of branch and bound
            root = solve_data.working_model = solve_data.original_model.clone()

            # get objective sense
            process_objective(solve_data, config)
            objectives = solve_data.original_model.component_data_objects(
                Objective, active=True)
            obj = next(objectives, None)
            solve_data.results.problem.sense = obj.sense

            # set up lists to keep track of which disjunctions have been covered.

            # this list keeps track of the relaxed disjunctions
            root.GDPbb_utils.unenforced_disjunctions = list(
                disjunction
                for disjunction in root.GDPbb_utils.disjunction_list
                if disjunction.active)

            root.GDPbb_utils.deactivated_constraints = ComponentSet([
                constr
                for disjunction in root.GDPbb_utils.unenforced_disjunctions
                for disjunct in disjunction.disjuncts
                for constr in disjunct.component_data_objects(ctype=Constraint,
                                                              active=True)
                if constr.body.polynomial_degree() not in (1, 0)
            ])
            # Deactivate nonlinear constraints in unenforced disjunctions
            for constr in root.GDPbb_utils.deactivated_constraints:
                constr.deactivate()

            # Add the BigM suffix if it does not already exist. Used later during nonlinear constraint activation.
            if not hasattr(root, 'BigM'):
                root.BigM = Suffix()

            # Pre-screen that none of the disjunctions are already predetermined due to the disjuncts being fixed
            # to True/False values.
            # TODO this should also be done within the loop, but we aren't handling it right now.
            # Should affect efficiency, but not correctness.
            root.GDPbb_utils.disjuncts_fixed_True = ComponentSet()
            # Only find top-level (non-nested) disjunctions
            for disjunction in root.component_data_objects(Disjunction,
                                                           active=True):
                fixed_true_disjuncts = [
                    disjunct for disjunct in disjunction.disjuncts
                    if disjunct.indicator_var.fixed
                    and disjunct.indicator_var.value == 1
                ]
                fixed_false_disjuncts = [
                    disjunct for disjunct in disjunction.disjuncts
                    if disjunct.indicator_var.fixed
                    and disjunct.indicator_var.value == 0
                ]
                for disjunct in fixed_false_disjuncts:
                    disjunct.deactivate()
                if len(fixed_false_disjuncts) == len(
                        disjunction.disjuncts) - 1:
                    # all but one disjunct in the disjunction is fixed to False. Remaining one must be true.
                    if not fixed_true_disjuncts:
                        fixed_true_disjuncts = [
                            disjunct for disjunct in disjunction.disjuncts
                            if disjunct not in fixed_false_disjuncts
                        ]
                # Reactivate the fixed-true disjuncts
                for disjunct in fixed_true_disjuncts:
                    newly_activated = ComponentSet()
                    for constr in disjunct.component_data_objects(Constraint):
                        if constr in root.GDPbb_utils.deactivated_constraints:
                            newly_activated.add(constr)
                            constr.activate()
                            # Set the big M value for the constraint
                            root.BigM[constr] = 1
                            # Note: we use a default big M value of 1
                            # because all non-selected disjuncts should be deactivated.
                            # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed.
                            # The default M value should therefore be irrelevant.
                    root.GDPbb_utils.deactivated_constraints -= newly_activated
                    root.GDPbb_utils.disjuncts_fixed_True.add(disjunct)

                if fixed_true_disjuncts:
                    assert disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \
                        "%s violates this assumption." % (disjunction.name, )
                    root.GDPbb_utils.unenforced_disjunctions.remove(
                        disjunction)

            # Check satisfiability
            if config.check_sat and satisfiable(root, config.logger) is False:
                # Problem is not satisfiable. Problem is infeasible.
                obj_value = obj_sign * float('inf')
            else:
                # solve the root node
                config.logger.info("Solving the root node.")
                obj_value, result, var_values = self.subproblem_solve(
                    root, config)

            if obj_sign * obj_value == float('inf'):
                config.logger.info(
                    "Model was found to be infeasible at the root node. Elapsed %.2f seconds."
                    % get_main_elapsed_time(solve_data.timing))
                if solve_data.results.problem.sense == minimize:
                    solve_data.results.problem.lower_bound = float('inf')
                    solve_data.results.problem.upper_bound = None
                else:
                    solve_data.results.problem.lower_bound = None
                    solve_data.results.problem.upper_bound = float('-inf')
                solve_data.results.solver.timing = solve_data.timing
                solve_data.results.solver.iterations = 0
                solve_data.results.solver.termination_condition = tc.infeasible
                return solve_data.results

            # initialize minheap for Branch and Bound algorithm
            # Heap structure: (ordering tuple, model)
            # Ordering tuple: (objective value, disjunctions_left, -total_nodes_counter)
            #  - select solutions with lower objective value,
            #    then fewer disjunctions left to explore (depth first),
            #    then more recently encountered (tiebreaker)
            heap = []
            total_nodes_counter = 0
            disjunctions_left = len(root.GDPbb_utils.unenforced_disjunctions)
            heapq.heappush(heap,
                           ((obj_sign * obj_value, disjunctions_left,
                             -total_nodes_counter), root, result, var_values))

            # loop to branch through the tree
            while len(heap) > 0:
                # pop best model off of heap
                sort_tuple, incumbent_model, incumbent_results, incumbent_var_values = heapq.heappop(
                    heap)
                incumbent_obj_value, disjunctions_left, _ = sort_tuple

                config.logger.info(
                    "Exploring node with LB %.10g and %s inactive disjunctions."
                    % (incumbent_obj_value, disjunctions_left))

                # if all the originally active disjunctions are active, solve and
                # return solution
                if disjunctions_left == 0:
                    config.logger.info("Model solved.")
                    # Model is solved. Copy over solution values.
                    original_model = solve_data.original_model
                    for orig_var, val in zip(
                            original_model.GDPbb_utils.variable_list,
                            incumbent_var_values):
                        orig_var.value = val

                    solve_data.results.problem.lower_bound = incumbent_results.problem.lower_bound
                    solve_data.results.problem.upper_bound = incumbent_results.problem.upper_bound
                    solve_data.results.solver.timing = solve_data.timing
                    solve_data.results.solver.iterations = total_nodes_counter
                    solve_data.results.solver.termination_condition = incumbent_results.solver.termination_condition
                    return solve_data.results

                # Pick the next disjunction to branch on
                next_disjunction = incumbent_model.GDPbb_utils.unenforced_disjunctions[
                    0]
                config.logger.info("Branching on disjunction %s" %
                                   next_disjunction.name)
                assert next_disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \
                    "%s violates this assumption." % (next_disjunction.name, )

                new_nodes_counter = 0

                for i, disjunct in enumerate(next_disjunction.disjuncts):
                    # Create one branch for each of the disjuncts on the disjunction

                    if any(disj.indicator_var.fixed
                           and disj.indicator_var.value == 1
                           for disj in next_disjunction.disjuncts
                           if disj is not disjunct):
                        # If any other disjunct is fixed to 1 and an xor relationship applies,
                        # then this disjunct cannot be activated.
                        continue

                    # Check time limit
                    if get_main_elapsed_time(
                            solve_data.timing) >= config.time_limit:
                        if solve_data.results.problem.sense == minimize:
                            solve_data.results.problem.lower_bound = incumbent_obj_value
                            solve_data.results.problem.upper_bound = float(
                                'inf')
                        else:
                            solve_data.results.problem.lower_bound = float(
                                '-inf')
                            solve_data.results.problem.upper_bound = incumbent_obj_value
                        config.logger.info('GDPopt unable to converge bounds '
                                           'before time limit of {} seconds. '
                                           'Elapsed: {} seconds'.format(
                                               config.time_limit,
                                               get_main_elapsed_time(
                                                   solve_data.timing)))
                        config.logger.info(
                            'Final bound values: LB: {}  UB: {}'.format(
                                solve_data.results.problem.lower_bound,
                                solve_data.results.problem.upper_bound))
                        solve_data.results.solver.timing = solve_data.timing
                        solve_data.results.solver.iterations = total_nodes_counter
                        solve_data.results.solver.termination_condition = tc.maxTimeLimit
                        return solve_data.results

                    # Branch on the disjunct
                    child = incumbent_model.clone()
                    # TODO I am leaving the old branching system in place, but there should be
                    # something better, ideally that deals with nested disjunctions as well.
                    disjunction_to_branch = child.GDPbb_utils.unenforced_disjunctions.pop(
                        0)
                    child_disjunct = disjunction_to_branch.disjuncts[i]
                    child_disjunct.indicator_var.fix(1)
                    # Deactivate (and fix to 0) other disjuncts on the disjunction
                    for disj in disjunction_to_branch.disjuncts:
                        if disj is not child_disjunct:
                            disj.deactivate()
                    # Activate nonlinear constraints on the newly fixed child disjunct
                    newly_activated = ComponentSet()
                    for constr in child_disjunct.component_data_objects(
                            Constraint):
                        if constr in child.GDPbb_utils.deactivated_constraints:
                            newly_activated.add(constr)
                            constr.activate()
                            # Set the big M value for the constraint
                            child.BigM[constr] = 1
                            # Note: we use a default big M value of 1
                            # because all non-selected disjuncts should be deactivated.
                            # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed.
                            # The default M value should therefore be irrelevant.
                    child.GDPbb_utils.deactivated_constraints -= newly_activated
                    child.GDPbb_utils.disjuncts_fixed_True.add(child_disjunct)

                    if disjunct in incumbent_model.GDPbb_utils.disjuncts_fixed_True:
                        # If the disjunct was already branched to True from a parent disjunct branching, just pass
                        # through the incumbent value without resolving. The solution should be the same as the parent.
                        total_nodes_counter += 1
                        ordering_tuple = (obj_sign * incumbent_obj_value,
                                          disjunctions_left - 1,
                                          -total_nodes_counter)
                        heapq.heappush(heap, (ordering_tuple, child, result,
                                              incumbent_var_values))
                        new_nodes_counter += 1
                        continue

                    if config.check_sat and satisfiable(
                            child, config.logger) is False:
                        # Problem is not satisfiable. Skip this disjunct.
                        continue

                    obj_value, result, var_values = self.subproblem_solve(
                        child, config)
                    total_nodes_counter += 1
                    ordering_tuple = (obj_sign * obj_value,
                                      disjunctions_left - 1,
                                      -total_nodes_counter)
                    heapq.heappush(heap,
                                   (ordering_tuple, child, result, var_values))
                    new_nodes_counter += 1

                config.logger.info(
                    "Added %s new nodes with %s relaxed disjunctions to the heap. Size now %s."
                    % (new_nodes_counter, disjunctions_left - 1, len(heap)))

    @staticmethod
    def validate_model(model):
        # Validates that model has only exclusive disjunctions
        for d in model.component_data_objects(ctype=Disjunction, active=True):
            if not d.xor:
                raise ValueError('GDPbb solver unable to handle '
                                 'non-exclusive disjunctions')
        objectives = model.component_data_objects(Objective, active=True)
        obj = next(objectives, None)
        if next(objectives, None) is not None:
            raise RuntimeError(
                "GDPbb solver is unable to handle model with multiple active objectives."
            )
        if obj is None:
            raise RuntimeError(
                "GDPbb solver is unable to handle model with no active objective."
            )

    @staticmethod
    def subproblem_solve(gdp, config):
        subproblem = gdp.clone()
        TransformationFactory('gdp.bigm').apply_to(subproblem)
        main_obj = next(
            subproblem.component_data_objects(Objective, active=True))
        obj_sign = 1 if main_obj.sense == minimize else -1

        try:
            result = SolverFactory(config.solver).solve(
                subproblem, **config.solver_args)
        except RuntimeError as e:
            config.logger.warning(
                "Solver encountered RuntimeError. Treating as infeasible. "
                "Msg: %s\n%s" % (str(e), traceback.format_exc()))
            var_values = [
                v.value for v in subproblem.GDPbb_utils.variable_list
            ]
            return obj_sign * float('inf'), SolverResults(), var_values

        var_values = [v.value for v in subproblem.GDPbb_utils.variable_list]
        term_cond = result.solver.termination_condition
        if result.solver.status is SolverStatus.ok and any(
                term_cond == valid_cond
                for valid_cond in (tc.optimal, tc.locallyOptimal,
                                   tc.feasible)):
            return value(main_obj.expr), result, var_values
        elif term_cond == tc.unbounded:
            return obj_sign * float('-inf'), result, var_values
        elif term_cond == tc.infeasible:
            return obj_sign * float('inf'), result, var_values
        else:
            config.logger.warning("Unknown termination condition of %s" %
                                  term_cond)
            return obj_sign * float('inf'), result, var_values

    def __enter__(self):
        return self

    def __exit__(self, t, v, traceback):
        pass
Esempio n. 29
0
def _get_MindtPy_config():
    """Set up the configurations for MindtPy.

    Returns
    -------
    CONFIG : ConfigBlock
        The specific configurations for MindtPy
    """
    CONFIG = ConfigBlock('MindtPy')

    CONFIG.declare(
        'iteration_limit',
        ConfigValue(
            default=50,
            domain=NonNegativeInt,
            description='Iteration limit',
            doc='Number of maximum iterations in the decomposition methods.'))
    CONFIG.declare(
        'stalling_limit',
        ConfigValue(
            default=15,
            domain=PositiveInt,
            description='Stalling limit',
            doc=
            'Stalling limit for primal bound progress in the decomposition methods.'
        ))
    CONFIG.declare(
        'time_limit',
        ConfigValue(
            default=600,
            domain=PositiveInt,
            description='Time limit (seconds, default=600)',
            doc='Seconds allowed until terminated. Note that the time limit can'
            'currently only be enforced between subsolver invocations. You may'
            'need to set subsolver time limits as well.'))
    CONFIG.declare(
        'strategy',
        ConfigValue(
            default='OA',
            domain=In(['OA', 'ECP', 'GOA', 'FP']),
            description='Decomposition strategy',
            doc='MINLP Decomposition strategy to be applied to the method. '
            'Currently available Outer Approximation (OA), Extended Cutting '
            'Plane (ECP), Global Outer Approximation (GOA) and Feasibility Pump (FP).'
        ))
    CONFIG.declare(
        'add_regularization',
        ConfigValue(
            default=None,
            domain=In([
                'level_L1', 'level_L2', 'level_L_infinity', 'grad_lag',
                'hess_lag', 'hess_only_lag', 'sqp_lag'
            ]),
            description='add regularization',
            doc=
            'Solving a regularization problem before solve the fixed subproblem'
            'the objective function of the regularization problem.'))
    CONFIG.declare(
        'init_strategy',
        ConfigValue(
            default=None,
            domain=In(['rNLP', 'initial_binary', 'max_binary', 'FP']),
            description='Initialization strategy',
            doc='Initialization strategy used by any method. Currently the '
            'continuous relaxation of the MINLP (rNLP), solve a maximal '
            'covering problem (max_binary), and fix the initial value for '
            'the integer variables (initial_binary).'))
    CONFIG.declare(
        'max_slack',
        ConfigValue(
            default=1000.0,
            domain=PositiveFloat,
            description='Maximum slack variable',
            doc=
            'Maximum slack variable value allowed for the Outer Approximation '
            'cuts.'))
    CONFIG.declare(
        'OA_penalty_factor',
        ConfigValue(
            default=1000.0,
            domain=PositiveFloat,
            description='Outer Approximation slack penalty factor',
            doc=
            'In the objective function of the Outer Approximation method, the '
            'slack variables corresponding to all the constraints get '
            'multiplied by this number and added to the objective.'))
    CONFIG.declare(
        'call_after_main_solve',
        ConfigValue(
            default=_DoNothing(),
            domain=None,
            description='Function to be executed after every main problem',
            doc='Callback hook after a solution of the main problem.'))
    CONFIG.declare(
        'call_after_subproblem_solve',
        ConfigValue(
            default=_DoNothing(),
            domain=None,
            description='Function to be executed after every subproblem',
            doc='Callback hook after a solution of the nonlinear subproblem.'))
    CONFIG.declare(
        'call_after_subproblem_feasible',
        ConfigValue(default=_DoNothing(),
                    domain=None,
                    description=
                    'Function to be executed after every feasible subproblem',
                    doc='Callback hook after a feasible solution'
                    ' of the nonlinear subproblem.'))
    CONFIG.declare(
        'tee',
        ConfigValue(default=False,
                    description='Stream output to terminal.',
                    domain=bool))
    CONFIG.declare(
        'logger',
        ConfigValue(
            default='pyomo.contrib.mindtpy',
            description='The logger object or name to use for reporting.',
            domain=a_logger))
    CONFIG.declare(
        'logging_level',
        ConfigValue(
            default=logging.INFO,
            domain=NonNegativeInt,
            description='The logging level for MindtPy.'
            'CRITICAL = 50, ERROR = 40, WARNING = 30, INFO = 20, DEBUG = 10, NOTSET = 0',
        ))
    CONFIG.declare(
        'integer_to_binary',
        ConfigValue(
            default=False,
            description=
            'Convert integer variables to binaries (for no-good cuts).',
            domain=bool))
    CONFIG.declare(
        'add_no_good_cuts',
        ConfigValue(
            default=False,
            description=
            'Add no-good cuts (no-good cuts) to binary variables to disallow same integer solution again.'
            'Note that integer_to_binary flag needs to be used to apply it to actual integers and not just binaries.',
            domain=bool))
    CONFIG.declare(
        'use_tabu_list',
        ConfigValue(
            default=False,
            description=
            'Use tabu list and incumbent callback to disallow same integer solution again.',
            domain=bool))
    CONFIG.declare(
        'add_affine_cuts',
        ConfigValue(default=False,
                    description='Add affine cuts drive from MC++.',
                    domain=bool))
    CONFIG.declare(
        'single_tree',
        ConfigValue(
            default=False,
            description=
            'Use single tree implementation in solving the MIP main problem.',
            domain=bool))
    CONFIG.declare(
        'solution_pool',
        ConfigValue(
            default=False,
            description='Use solution pool in solving the MIP main problem.',
            domain=bool))
    CONFIG.declare(
        'num_solution_iteration',
        ConfigValue(
            default=5,
            description=
            'The number of MIP solutions (from the solution pool) used to generate the fixed NLP subproblem in each iteration.',
            domain=PositiveInt))
    CONFIG.declare(
        'add_slack',
        ConfigValue(
            default=False,
            description='Whether add slack variable here.'
            'slack variables here are used to deal with nonconvex MINLP.',
            domain=bool))
    CONFIG.declare(
        'cycling_check',
        ConfigValue(
            default=True,
            description=
            'Check if OA algorithm is stalled in a cycle and terminate.',
            domain=bool))
    CONFIG.declare(
        'feasibility_norm',
        ConfigValue(
            default='L_infinity',
            domain=In(['L1', 'L2', 'L_infinity']),
            description=
            'Different forms of objective function in feasibility subproblem.')
    )
    CONFIG.declare(
        'differentiate_mode',
        ConfigValue(default='reverse_symbolic',
                    domain=In(['reverse_symbolic', 'sympy']),
                    description='Differentiate mode to calculate jacobian.'))
    CONFIG.declare(
        'linearize_inactive',
        ConfigValue(default=False,
                    description='Add OA cuts for inactive constraints.',
                    domain=bool))
    CONFIG.declare(
        'use_mcpp',
        ConfigValue(
            default=False,
            description=
            "Use package MC++ to set a bound for variable 'objective_value', which is introduced when the original problem's objective function is nonlinear.",
            domain=bool))
    CONFIG.declare(
        'equality_relaxation',
        ConfigValue(
            default=False,
            description=
            'Use dual solution from the NLP solver to add OA cuts for equality constraints.',
            domain=bool))
    CONFIG.declare(
        'calculate_dual',
        ConfigValue(default=False,
                    description='Calculate duals of the NLP subproblem.',
                    domain=bool))
    CONFIG.declare(
        'use_fbbt',
        ConfigValue(default=False,
                    description=
                    'Use fbbt to tighten the feasible region of the problem.',
                    domain=bool))
    CONFIG.declare(
        'use_dual_bound',
        ConfigValue(
            default=True,
            description=
            'Add dual bound constraint to enforce the objective satisfies best-found dual bound.',
            domain=bool))
    CONFIG.declare(
        'heuristic_nonconvex',
        ConfigValue(
            default=False,
            description=
            'Use dual solution from the NLP solver and slack variables to add OA cuts for equality constraints (Equality relaxation)'
            'and minimize the sum of the slack variables (Augmented Penalty).',
            domain=bool))
    CONFIG.declare(
        'partition_obj_nonlinear_terms',
        ConfigValue(
            default=True,
            description=
            'Partition objective with the sum of nonlinear terms using epigraph reformulation.',
            domain=bool))

    _add_subsolver_configs(CONFIG)
    _add_tolerance_configs(CONFIG)
    _add_fp_configs(CONFIG)
    _add_bound_configs(CONFIG)
    _add_loa_configs(CONFIG)
    return CONFIG
Esempio n. 30
0
class EvaporatorData(UnitModelBlockData):
    """
    Evaporator model for MVC
    """

    # CONFIG are options for the unit model, this simple model only has the mandatory config options
    CONFIG = ConfigBlock()

    CONFIG.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag - must be False",
            doc="""Indicates whether this model will be dynamic or not,
    **default** = False. The filtration unit does not support dynamic
    behavior, thus this must be False.""",
        ),
    )
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag - must be False",
            doc="""Indicates whether holdup terms should be constructed or not.
    **default** - False. The filtration unit does not have defined volume, thus
    this must be False.""",
        ),
    )
    CONFIG.declare(
        "property_package_feed",
        ConfigValue(
            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,
    **PhysicalParameterObject** - a PhysicalParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "property_package_args_feed",
        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(
        "property_package_vapor",
        ConfigValue(
            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,
        **PhysicalParameterObject** - a PhysicalParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "property_package_args_vapor",
        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(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
        **default** - MaterialBalanceType.useDefault.
        **Valid values:** {
        **MaterialBalanceType.useDefault - refer to property package for default
        balance type
        **MaterialBalanceType.none** - exclude material balances,
        **MaterialBalanceType.componentPhase** - use phase component balances,
        **MaterialBalanceType.componentTotal** - use total component balances,
        **MaterialBalanceType.elementTotal** - use total element balances,
        **MaterialBalanceType.total** - use total material balance.}""",
        ),
    )
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
        **default** - EnergyBalanceType.useDefault.
        **Valid values:** {
        **EnergyBalanceType.useDefault - refer to property package for default
        balance type
        **EnergyBalanceType.none** - exclude energy balances,
        **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
        **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
        **EnergyBalanceType.energyTotal** - single energy balance for material,
        **EnergyBalanceType.energyPhase** - energy balances for each phase.}""",
        ),
    )
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
        **default** - MomentumBalanceType.pressureTotal.
        **Valid values:** {
        **MomentumBalanceType.none** - exclude momentum balances,
        **MomentumBalanceType.pressureTotal** - single pressure balance for material,
        **MomentumBalanceType.pressurePhase** - pressure balances for each phase,
        **MomentumBalanceType.momentumTotal** - single momentum balance for material,
        **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""",
        ),
    )

    def build(self):
        super().build()

        if self.config.property_package_feed is None:
            raise ConfigurationError(
                "Users must provide a feed property package to the evaporator unit model"
            )
        if self.config.property_package_vapor is None:
            raise ConfigurationError(
                "Users must provide a vapor property package to the evaporator unit model"
            )

        # this creates blank scaling factors, which are populated later
        self.scaling_factor = Suffix(direction=Suffix.EXPORT)

        # Next, get the base units of measurement from the property definition
        units_meta_feed = (
            self.config.property_package_feed.get_metadata().get_derived_units)

        # Add shared unit model variables
        self.U = Var(
            initialize=1e3,
            bounds=(10, 1e4),
            units=pyunits.J * pyunits.s**-1 * pyunits.m**-2 * pyunits.K**-1,
        )

        self.area = Var(initialize=1e2, bounds=(1e-1, 1e4), units=pyunits.m**2)

        self.delta_temperature_in = Var(initialize=1e1,
                                        bounds=(1e-8, 1e3),
                                        units=pyunits.K)

        self.delta_temperature_out = Var(initialize=1e1,
                                         bounds=(1e-8, 1e3),
                                         units=pyunits.K)

        self.lmtd = Var(initialize=1e1, bounds=(1e-8, 1e3), units=pyunits.K)

        # Add feed_side block
        self.feed_side = Block()

        # Add unit variables to feed
        self.feed_side.heat_transfer = Var(initialize=1e4,
                                           bounds=(1, 1e10),
                                           units=pyunits.J * pyunits.s**-1)

        # Add feed_side state blocks
        # Feed state block
        tmp_dict = dict(**self.config.property_package_args_feed)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["parameters"] = self.config.property_package_feed
        tmp_dict["defined_state"] = True  # feed inlet defined
        self.feed_side.properties_feed = (
            self.config.property_package_feed.state_block_class(
                self.flowsheet().config.time,
                doc="Material properties of feed inlet",
                default=tmp_dict,
            ))

        # Brine state block
        tmp_dict["defined_state"] = False  # brine outlet not yet defined
        self.feed_side.properties_brine = (
            self.config.property_package_feed.state_block_class(
                self.flowsheet().config.time,
                doc="Material properties of brine outlet",
                default=tmp_dict,
            ))

        # Vapor state block
        tmp_dict = dict(**self.config.property_package_args_vapor)
        tmp_dict["has_phase_equilibrium"] = False
        tmp_dict["parameters"] = self.config.property_package_vapor
        tmp_dict["defined_state"] = False  # vapor outlet not yet defined
        self.feed_side.properties_vapor = (
            self.config.property_package_vapor.state_block_class(
                self.flowsheet().config.time,
                doc="Material properties of vapor outlet",
                default=tmp_dict,
            ))

        # Add condenser
        self.condenser = Condenser(
            default={"property_package": self.config.property_package_vapor})

        # Add ports - oftentimes users interact with these rather than the state blocks
        self.add_port(name="inlet_feed", block=self.feed_side.properties_feed)
        self.add_port(name="outlet_brine",
                      block=self.feed_side.properties_brine)
        self.add_port(name="outlet_vapor",
                      block=self.feed_side.properties_vapor)
        self.add_port(name="inlet_condenser",
                      block=self.condenser.control_volume.properties_in)
        self.add_port(name="outlet_condenser",
                      block=self.condenser.control_volume.properties_out)

        ### FEED SIDE CONSTRAINTS ###
        # Mass balance
        @self.feed_side.Constraint(
            self.flowsheet().time,
            self.config.property_package_feed.component_list,
            doc="Mass balance",
        )
        def eq_mass_balance(b, t, j):
            lb = b.properties_vapor[t].flow_mass_phase_comp["Liq", "H2O"].lb
            b.properties_vapor[t].flow_mass_phase_comp["Liq", "H2O"].fix(lb)
            if j == "H2O":
                return (
                    b.properties_feed[t].flow_mass_phase_comp["Liq", "H2O"] ==
                    b.properties_brine[t].flow_mass_phase_comp["Liq", "H2O"] +
                    b.properties_vapor[t].flow_mass_phase_comp["Vap", "H2O"])
            else:
                return (b.properties_feed[t].flow_mass_phase_comp["Liq", j] ==
                        b.properties_brine[t].flow_mass_phase_comp["Liq", j])

        # Energy balance
        @self.feed_side.Constraint(self.flowsheet().time, doc="Energy balance")
        def eq_energy_balance(b, t):
            return (b.heat_transfer + b.properties_feed[t].enth_flow ==
                    b.properties_brine[t].enth_flow +
                    b.properties_vapor[t].enth_flow_phase["Vap"])

        # Brine pressure
        @self.feed_side.Constraint(self.flowsheet().time, doc="Brine pressure")
        def eq_brine_pressure(b, t):
            return b.properties_brine[t].pressure == b.properties_brine[
                t].pressure_sat

        # Vapor pressure
        @self.feed_side.Constraint(self.flowsheet().time, doc="Vapor pressure")
        def eq_vapor_pressure(b, t):
            return b.properties_vapor[t].pressure == b.properties_brine[
                t].pressure

        # Vapor temperature
        @self.feed_side.Constraint(self.flowsheet().time,
                                   doc="Vapor temperature")
        def eq_vapor_temperature(b, t):
            return (b.properties_vapor[t].temperature ==
                    b.properties_brine[t].temperature)
            # return b.properties_vapor[t].temperature == 0.5*(b.properties_out[t].temperature + b.properties_in[t].temperature)

        ### EVAPORATOR CONSTRAINTS ###
        # Temperature difference in
        @self.Constraint(self.flowsheet().time,
                         doc="Temperature difference in")
        def eq_delta_temperature_in(b, t):
            return (b.delta_temperature_in ==
                    b.condenser.control_volume.properties_in[t].temperature -
                    b.feed_side.properties_brine[t].temperature)

        # Temperature difference out
        @self.Constraint(self.flowsheet().time,
                         doc="Temperature difference out")
        def eq_delta_temperature_out(b, t):
            return (b.delta_temperature_out ==
                    b.condenser.control_volume.properties_out[t].temperature -
                    b.feed_side.properties_brine[t].temperature)

        # log mean temperature
        @self.Constraint(self.flowsheet().time,
                         doc="Log mean temperature difference")
        def eq_lmtd(b, t):
            dT_in = b.delta_temperature_in
            dT_out = b.delta_temperature_out
            temp_units = pyunits.get_units(dT_in)
            dT_avg = (dT_in + dT_out) / 2
            # external function that ruturns the real root, for the cuberoot of negitive
            # numbers, so it will return without error for positive and negitive dT.
            b.cbrt = ExternalFunction(library=functions_lib(),
                                      function="cbrt",
                                      arg_units=[temp_units**3])
            return b.lmtd == b.cbrt((dT_in * dT_out * dT_avg)) * temp_units

        # Heat transfer between feed side and condenser
        @self.Constraint(self.flowsheet().time, doc="Heat transfer balance")
        def eq_heat_balance(b, t):
            return b.feed_side.heat_transfer == -b.condenser.control_volume.heat[
                t]

        # Evaporator heat transfer
        @self.Constraint(self.flowsheet().time, doc="Evaporator heat transfer")
        def eq_evaporator_heat(b, t):
            return b.feed_side.heat_transfer == b.U * b.area * b.lmtd

    def initialize(blk,
                   state_args=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        """
        General wrapper for pressure changer initialization routines
        Keyword Arguments:
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for
                         initialization (see documentation of the specific
                         property package) (default = {}).
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default=None)
            solver : str indicating which solver to use during
                     initialization (default = None)
        Returns: None
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
        # Set solver options
        opt = get_solver(solver, optarg)

        # ---------------------------------------------------------------------
        # Initialize feed side
        flags_feed = blk.feed_side.properties_feed.initialize(solver=solver,
                                                              optarg=optarg,
                                                              hold_state=True)
        init_log.info_high("Initialization Step 1 Complete.")
        # # ---------------------------------------------------------------------
        # # Initialize brine
        # Set state_args from inlet state
        if state_args is None:
            state_args = {}
            state_dict = blk.feed_side.properties_feed[
                blk.flowsheet().config.time.first()].define_port_members()

            for k in state_dict.keys():
                if state_dict[k].is_indexed():
                    state_args[k] = {}
                    for m in state_dict[k].keys():
                        state_args[k][m] = state_dict[k][m].value
                else:
                    state_args[k] = state_dict[k].value

        blk.feed_side.properties_brine.initialize(outlvl=outlvl,
                                                  optarg=optarg,
                                                  solver=solver,
                                                  state_args=state_args)

        state_args_vapor = {}
        state_args_vapor["pressure"] = 0.5 * state_args["pressure"]
        state_args_vapor["temperature"] = state_args["temperature"]
        state_args_vapor["flow_mass_phase_comp"] = {
            ("Liq", "H2O"):
            blk.feed_side.properties_vapor[0].flow_mass_phase_comp["Liq",
                                                                   "H2O"].lb,
            ("Vap", "H2O"):
            state_args["flow_mass_phase_comp"][("Liq", "H2O")],
        }

        blk.feed_side.properties_vapor.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args_vapor,
        )

        init_log.info_high("Initialization Step 2 Complete.")

        # intialize condenser
        state_args_condenser = state_args_vapor
        state_args_condenser["flow_mass_phase_comp"][("Vap", "H2O")] = (
            0.5 * state_args_condenser["flow_mass_phase_comp"][("Vap", "H2O")])
        state_args_condenser["pressure"] = blk.feed_side.properties_brine[
            0].pressure_sat.value
        state_args_condenser["temperature"] = state_args["temperature"] + 5
        blk.condenser.initialize(state_args=state_args_condenser)
        # assert False
        # flags_condenser_cv = blk.condenser.initialize(state_args=state_args_condenser,hold_state=True)
        init_log.info_high("Initialization Step 3 Complete.")
        # ---------------------------------------------------------------------
        # Deactivate heat transfer balance
        # blk.eq_heat_balance.deactivate()

        # Solve unit
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 4 {}.".format(
            idaeslog.condition(res)))

        # ---------------------------------------------------------------------
        # Release feed and condenser inlet states
        blk.feed_side.properties_feed.release_state(flags_feed, outlvl=outlvl)
        # blk.condenser.control_volume.release_state(flags_condenser_cv, outlvl=outlvl)

        init_log.info("Initialization Complete: {}".format(
            idaeslog.condition(res)))

    def _get_performance_contents(self, time_point=0):
        var_dict = {
            "Heat transfer": self.feed_side.heat_transfer,
            "Evaporator temperature":
            self.feed_side.properties_brine[0].temperature,
            "Evaporator pressure": self.feed_side.properties_brine[0].pressure,
        }

        return {"vars": var_dict}

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()

        self.condenser.calculate_scaling_factors()

        if iscale.get_scaling_factor(self.feed_side.heat_transfer) is None:
            sf = iscale.get_scaling_factor(
                self.feed_side.properties_vapor[0].enth_flow_phase["Vap"])
            iscale.set_scaling_factor(self.feed_side.heat_transfer, sf)