Esempio n. 1
0
    def test_In(self):
        c = ConfigBlock()
        c.declare('a', ConfigValue(None, In([1,3,5])))
        self.assertEqual(c.a, None)
        c.a = 3
        self.assertEqual(c.a, 3)
        with self.assertRaises(ValueError):
            c.a = 2
        self.assertEqual(c.a, 3)
        with self.assertRaises(ValueError):
            c.a = {}
        self.assertEqual(c.a, 3)
        with self.assertRaises(ValueError):
            c.a = '1'
        self.assertEqual(c.a, 3)

        c.declare('b', ConfigValue(None, In([1,3,5], int)))
        self.assertEqual(c.b, None)
        c.b = 3
        self.assertEqual(c.b, 3)
        with self.assertRaises(ValueError):
            c.b = 2
        self.assertEqual(c.b, 3)
        with self.assertRaises(ValueError):
            c.b = {}
        self.assertEqual(c.b, 3)
        c.b = '1'
        self.assertEqual(c.b, 1)
Esempio n. 2
0
def _make_pem_electrolyzer_config_block(config):
    config.declare("dynamic", ConfigValue(
        domain=In([False]),
        default=False,
        description="Dynamic model flag - must be False",
        doc="""PEM Electrolyzer does 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("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. 3
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. 4
0
def _define_config(config):
    config.compressor = False
    config.get("compressor")._default = False
    config.get("compressor")._domain = In([False])
    config.material_balance_type = MaterialBalanceType.componentTotal
    config.get(
        "material_balance_type")._default = MaterialBalanceType.componentTotal
    config.thermodynamic_assumption = ThermodynamicAssumption.adiabatic
    config.get("thermodynamic_assumption"
               )._default = ThermodynamicAssumption.adiabatic
    config.get("thermodynamic_assumption")._domain = In(
        [ThermodynamicAssumption.adiabatic])
    config.declare(
        "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(
        "valve_function_rule",
        ConfigValue(
            default=None,
            description=
            "This is a rule that returns a time indexed valve function expression.",
            doc=
            """This is a rule that returns a time indexed valve function expression.
This is required only if valve_function==ValveFunctionType.custom""",
        ),
    )
    config.declare(
        "phase",
        ConfigValue(
            default="Vap",
            domain=In(("Vap", "Liq")),
            description='Expected phase of fluid in valve in {"Liq", "Vap"}',
        ),
    )
Esempio n. 5
0
def _add_fp_configs(CONFIG):
    """Adds the feasibility pump-related configurations.

    Parameters
    ----------
    CONFIG : ConfigBlock
        The specific configurations for MindtPy.
    """
    CONFIG.declare('fp_cutoffdecr', ConfigValue(
        default=1E-1,
        domain=PositiveFloat,
        description='Additional relative decrement of cutoff value for the original objective function.'
    ))
    CONFIG.declare('fp_iteration_limit', ConfigValue(
        default=20,
        domain=PositiveInt,
        description='Feasibility pump iteration limit',
        doc='Number of maximum iterations in the feasibility pump methods.'
    ))
    # TODO: integrate this option
    CONFIG.declare('fp_projcuts', ConfigValue(
        default=True,
        description='Whether to add cut derived from regularization of MIP solution onto NLP feasible set.',
        domain=bool
    ))
    CONFIG.declare('fp_transfercuts', ConfigValue(
        default=True,
        description='Whether to transfer cuts from the Feasibility Pump MIP to main MIP in selected strategy (all except from the round in which the FP MIP became infeasible).',
        domain=bool
    ))
    CONFIG.declare('fp_projzerotol', ConfigValue(
        default=1E-4,
        domain=PositiveFloat,
        description='Tolerance on when to consider optimal value of regularization problem as zero, which may trigger the solution of a Sub-NLP.'
    ))
    CONFIG.declare('fp_mipgap', ConfigValue(
        default=1E-2,
        domain=PositiveFloat,
        description='Optimality tolerance (relative gap) to use for solving MIP regularization problem.'
    ))
    CONFIG.declare('fp_discrete_only', ConfigValue(
        default=True,
        description='Only calculate the distance among discrete variables in regularization problems.',
        domain=bool
    ))
    CONFIG.declare('fp_main_norm', ConfigValue(
        default='L1',
        domain=In(['L1', 'L2', 'L_infinity']),
        description='Different forms of objective function MIP regularization problem.'
    ))
    CONFIG.declare('fp_norm_constraint', ConfigValue(
        default=True,
        description='Whether to add the norm constraint to FP-NLP',
        domain=bool
    ))
    CONFIG.declare('fp_norm_constraint_coef', ConfigValue(
        default=1,
        domain=PositiveFloat,
        description='The coefficient in the norm constraint, correspond to the Beta in the paper.'
    ))
Esempio n. 6
0
def _make_heat_exchanger_config(config):
    """
    Declare configuration options for HeatExchangerData block.
    """
    config.declare("side_1", ConfigBlock(
        implicit=True,
        description="Config block for side_1",
        doc="""A config block used to construct the side_1 control volume."""))
    config.declare("side_2", ConfigBlock(
        implicit=True,
        description="Config block for side_2",
        doc="""A config block used to construct the side_2 control volume."""))
    _make_heater_config_block(config.side_1)
    _make_heater_config_block(config.side_2)
    config.declare("delta_temperature_callback", ConfigValue(
        default=delta_temperature_lmtd_callback,
        description="Callback for for temperature difference calculations"))
    config.declare("flow_pattern", ConfigValue(
        default=HeatExchangerFlowPattern.countercurrent,
        domain=In(HeatExchangerFlowPattern),
        description="Heat exchanger flow pattern",
        doc="""Heat exchanger flow pattern,
**default** - HeatExchangerFlowPattern.countercurrent.
**Valid values:** {
**HeatExchangerFlowPattern.countercurrent** - countercurrent flow,
**HeatExchangerFlowPattern.cocurrent** - cocurrent flow,
**HeatExchangerFlowPattern.crossflow** - cross flow, factor times
countercurrent temperature difference.}"""))
Esempio n. 7
0
class IonData(SoluteData):
    """
    Component type for ionic species. These can exist only in AqueousPhases,
    and are always solutes.
    """
    CONFIG = SoluteData.CONFIG()

    # Remove valid_phase_types argument, as ions are aqueous phase only
    CONFIG.__delitem__("valid_phase_types")
    # Set as not having a vapor pressure
    has_psat = CONFIG.get("has_vapor_pressure")
    has_psat.set_value(False)
    has_psat.set_default_value(False)
    has_psat.set_domain(In([False]))

    CONFIG.declare("charge",
                   ConfigValue(domain=int, doc="Charge of ionic species."))

    def _is_phase_valid(self, phase):
        return phase.is_aqueous_phase()

    def _is_aqueous_phase_valid(self):
        return True

    def _add_to_electrolyte_component_list(self):
        """
        Special case method for adding references to new Component in
        component_lists for electrolyte systems,

        New Component types should overload this method
        """
        raise NotImplementedError(
            "{} The IonData component class is inteded as a base class for "
            "the AnionData and CationData classes, and should not be used "
            "directly".format(self.name))
class CompressorData(PressureChangerData):
    # Pressure changer with isentropic turbine options
    CONFIG = PressureChangerData.CONFIG()
    CONFIG.compressor = True
    CONFIG.get("compressor")._default = True
    CONFIG.get("compressor")._domain = In([True])
    CONFIG.thermodynamic_assumption = ThermodynamicAssumption.isentropic
    CONFIG.get("thermodynamic_assumption")._default = ThermodynamicAssumption.isentropic
Esempio n. 9
0
class EnergyRecoveryDeviceData(PumpIsothermalData):
    """
    Turbine-type isothermal energy recovery device
    """

    # switch compressor to False
    CONFIG = PumpIsothermalData.CONFIG()
    CONFIG.get("compressor")._default = False
    CONFIG.get("compressor")._domain = In([False])
    CONFIG.compressor = False
Esempio n. 10
0
def _add_loa_configs(CONFIG):
    """Adds the LOA-related configurations.

    Parameters
    ----------
    CONFIG : ConfigBlock
        The specific configurations for MindtPy.
    """
    CONFIG.declare(
        'level_coef',
        ConfigValue(
            default=0.5,
            domain=PositiveFloat,
            description='The coefficient in the regularization main problem'
            'represents how much the linear approximation of the MINLP problem is trusted.'
        ))
    CONFIG.declare(
        'solution_limit',
        ConfigValue(
            default=10,
            domain=PositiveInt,
            description=
            'The solution limit for the regularization problem since it does not need to be solved to optimality.'
        ))
    CONFIG.declare(
        'add_cuts_at_incumbent',
        ConfigValue(
            default=False,
            description=
            'Whether to add lazy cuts to the main problem at the incumbent solution found in the branch & bound tree',
            domain=bool))
    CONFIG.declare(
        'reduce_level_coef',
        ConfigValue(
            default=False,
            description=
            'Whether to reduce level coefficient in ROA single tree when regularization problem is infeasible.',
            domain=bool))
    CONFIG.declare(
        'use_bb_tree_incumbent',
        ConfigValue(
            default=False,
            description=
            'Whether to use the incumbent solution of branch & bound tree in ROA single tree when regularization problem is infeasible.',
            domain=bool))
    CONFIG.declare(
        'sqp_lag_scaling_coef',
        ConfigValue(
            default='fixed',
            domain=In(['fixed', 'variable_dependent']),
            description='The coefficient used to scale the L2 norm in sqp_lag.'
        ))
Esempio n. 11
0
def _get_GDPopt_config():
    _supported_strategies = {
        'LOA',  # Logic-based outer approximation
        'GLOA',  # Global logic-based outer approximation
        'LBB',  # Logic-based branch-and-bound
        'RIC',  # Relaxation with Integer Cuts
    }
    CONFIG = ConfigBlock("GDPopt")
    CONFIG.declare(
        "iterlim",
        ConfigValue(default=100,
                    domain=NonNegativeInt,
                    description="Iteration limit."))
    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=None,
                    domain=In(_supported_strategies),
                    description="Decomposition strategy to use."))
    CONFIG.declare(
        "tee",
        ConfigValue(default=False,
                    description="Stream output to terminal.",
                    domain=bool))
    CONFIG.declare(
        "logger",
        ConfigValue(
            default='pyomo.contrib.gdpopt',
            description="The logger object or name to use for reporting.",
            domain=a_logger))
    _add_OA_configs(CONFIG)
    _add_BB_configs(CONFIG)
    _add_subsolver_configs(CONFIG)
    _add_tolerance_configs(CONFIG)
    return CONFIG
Esempio n. 12
0
    def __init__(self, subproblem_solver):
        self._subproblem_solver = pe.SolverFactory(subproblem_solver)
        if isinstance(self._subproblem_solver, PersistentSolver):
            self._using_persistent_solver = True
        else:
            self._using_persistent_solver = False
        self._relaxations = ComponentSet()
        self._relaxations_not_tracking_solver = ComponentSet()
        self._relaxations_with_added_cuts = ComponentSet()
        self._pyomo_model = None

        self.options = ConfigBlock()
        self.options.declare(
            'feasibility_tol',
            ConfigValue(default=1e-6,
                        domain=NonNegativeFloat,
                        doc='Tolerance below which cuts will not be added'))
        self.options.declare(
            'max_iter',
            ConfigValue(default=30,
                        domain=NonNegativeInt,
                        doc='Maximum number of iterations'))
        self.options.declare(
            'keep_cuts',
            ConfigValue(
                default=False,
                domain=In([True, False]),
                doc='Whether or not to keep the cuts generated after the solve'
            ))
        self.options.declare(
            'time_limit',
            ConfigValue(default=float('inf'),
                        domain=NonNegativeFloat,
                        doc='Time limit in seconds'))

        self.subproblem_solver_options = ConfigBlock(implicit=True)
Esempio n. 13
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. 14
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. 15
0
class TrustRegionSolver(OptSolver):
    """
    A trust region filter method for black box / glass box optimizaiton
    Solves nonlinear optimization problems containing external function calls
    through automatic construction of reduced models (ROM), also known as
    surrogate models.
    Currently implements linear and quadratic reduced models.
    See Eason, Biegler (2016) AIChE Journal for more details

    Arguments:
    """
    #    + param.CONFIG.generte_yaml_template()

    CONFIG = ConfigBlock('Trust Region')

    CONFIG.declare(
        'solver',
        ConfigValue(default='ipopt',
                    description='solver to use, defaults to ipopt',
                    doc=''))

    CONFIG.declare(
        'solver_options',
        ConfigBlock(implicit=True,
                    description='options to pass to the subproblem solver',
                    doc=''))

    # Initialize trust radius
    CONFIG.declare(
        'trust radius',
        ConfigValue(default=1.0, domain=PositiveFloat, description='', doc=''))

    # Initialize sample region
    CONFIG.declare(
        'sample region',
        ConfigValue(default=True, domain=bool, description='', doc=''))

    # Initialize sample radius
    # TODO do we need to keep the if statement?
    if CONFIG.sample_region:
        default_sample_radius = 0.1
    else:
        default_sample_radius = CONFIG.trust_radius / 2.0

    CONFIG.declare(
        'sample radius',
        ConfigValue(default=default_sample_radius,
                    domain=PositiveFloat,
                    description='',
                    doc=''))

    # Initialize radius max
    CONFIG.declare(
        'radius max',
        ConfigValue(default=1000.0 * CONFIG.trust_radius,
                    domain=PositiveFloat,
                    description='',
                    doc=''))

    # Termination tolerances
    CONFIG.declare(
        'ep i',
        ConfigValue(default=1e-5, domain=PositiveFloat, description='',
                    doc=''))

    CONFIG.declare(
        'ep delta',
        ConfigValue(default=1e-5, domain=PositiveFloat, description='',
                    doc=''))

    CONFIG.declare(
        'ep chi',
        ConfigValue(default=1e-3, domain=PositiveFloat, description='',
                    doc=''))

    CONFIG.declare(
        'delta min',
        ConfigValue(default=1e-6,
                    domain=PositiveFloat,
                    description='delta min <= ep delta',
                    doc=''))

    CONFIG.declare(
        'max it',
        ConfigValue(default=20, domain=PositiveInt, description='', doc=''))

    # Compatibility Check Parameters
    CONFIG.declare(
        'kappa delta',
        ConfigValue(default=0.8, domain=PositiveFloat, description='', doc=''))

    CONFIG.declare(
        'kappa mu',
        ConfigValue(default=1.0, domain=PositiveFloat, description='', doc=''))

    CONFIG.declare(
        'mu',
        ConfigValue(default=0.5, domain=PositiveFloat, description='', doc=''))

    CONFIG.declare(
        'ep compatibility',
        ConfigValue(default=CONFIG.ep_i,
                    domain=PositiveFloat,
                    description='Suggested value: ep compatibility == ep i',
                    doc=''))

    CONFIG.declare(
        'compatibility penalty',
        ConfigValue(default=0.0,
                    domain=NonNegativeFloat,
                    description='',
                    doc=''))

    # Criticality Check Parameters
    CONFIG.declare(
        'criticality check',
        ConfigValue(default=0.1, domain=PositiveFloat, description='', doc=''))

    # Trust region update parameters
    CONFIG.declare(
        'gamma c',
        ConfigValue(default=0.5, domain=PositiveFloat, description='', doc=''))

    CONFIG.declare(
        'gamma e',
        ConfigValue(default=2.5, domain=PositiveFloat, description='', doc=''))

    # Switching Condition
    CONFIG.declare(
        'gamma s',
        ConfigValue(default=2.0, domain=PositiveFloat, description='', doc=''))

    CONFIG.declare(
        'kappa theta',
        ConfigValue(default=0.1, domain=PositiveFloat, description='', doc=''))

    CONFIG.declare(
        'theta min',
        ConfigValue(default=1e-4, domain=PositiveFloat, description='',
                    doc=''))

    # Filter
    CONFIG.declare(
        'gamma f',
        ConfigValue(
            default=0.01,
            domain=PositiveFloat,
            description='gamma_f and gamma_theta in (0,1) are fixed parameters',
            doc=''))

    CONFIG.declare(
        'gamma theta',
        ConfigValue(
            default=0.01,
            domain=PositiveFloat,
            description='gamma_f and gamma_theta in (0,1) are fixed parameters',
            doc=''))

    CONFIG.declare(
        'theta max',
        ConfigValue(default=50, domain=PositiveInt, description='', doc=''))

    # Ratio test parameters (for theta steps)
    CONFIG.declare(
        'eta1',
        ConfigValue(default=0.05, domain=PositiveFloat, description='',
                    doc=''))

    CONFIG.declare(
        'eta2',
        ConfigValue(default=0.2, domain=PositiveFloat, description='', doc=''))

    # Output level (replace with real printlevels!!!)
    CONFIG.declare(
        'print variables',
        ConfigValue(default=False, domain=bool, description='', doc=''))

    # Sample Radius reset parameter
    CONFIG.declare(
        'sample radius adjust',
        ConfigValue(default=0.5, domain=PositiveFloat, description='', doc=''))

    # Default romtype
    CONFIG.declare(
        'reduced model type',
        ConfigValue(default=1,
                    domain=In([0, 1]),
                    description='0 = Linear, 1 = Quadratic',
                    doc=''))

    def __init__(self, **kwds):
        # set persistent config options
        tmp_kwds = {'type': kwds.pop('type', 'trustregion')}
        self.config = self.CONFIG(kwds, preserve_implicit=True)

        #
        # Call base class constructor
        #

        tmp_kwds['solver'] = self.config.solver
        OptSolver.__init__(self, **tmp_kwds)

    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 a 3-tuple describing the solver version."""
        return __version__

    def solve(self, model, eflist, **kwds):
        # set customized config parameters
        self._local_config = self.config(kwds, preserve_implicit=True)

        # first store all data we will need to change in original model as a tuple
        # [0]=Var component, [1]=external function list, [2]=config block
        model._tmp_trf_data = (list(model.component_data_objects(Var)), eflist,
                               self._local_config)
        # now clone the model
        inst = model.clone()

        # call TRF on cloned model
        TRF(inst, inst._tmp_trf_data[1], inst._tmp_trf_data[2])

        # copy potentially changed variable values back to original model and return
        for inst_var, orig_var in zip(inst._tmp_trf_data[0],
                                      model._tmp_trf_data[0]):
            orig_var.set_value(value(inst_var))
Esempio n. 16
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. 17
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"
            }
        })
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. 19
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. 20
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. 21
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. 22
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)
Esempio n. 23
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. 24
0
class HelmIsentropicTurbineData(BalanceBlockData):
    """
    Basic isentropic 0D turbine model.  This inherits the heater block to get
    a lot of unit model boilerplate and the mass balance, enegy balance and
    pressure equations.  This model is intended to be used only with Helmholtz
    EOS property pacakges in mixed or single phase mode with P-H state vars.

    Since this inherits BalanceBlockData, and only operates in steady-state or
    pseudo-steady-state (for dynamic models) the following mass, energy and
    pressure equations are implicitly writen.

    1) Mass Balance:
        0 = flow_mol_in[t] - flow_mol_out[t]
    2) Energy Balance:
        0 = (flow_mol[t]*h_mol[t])_in - (flow_mol[t]*h_mol[t])_out + Q_in + W_in
    3) Pressure:
        0 = P_in[t] + deltaP[t] - P_out[t]
    """

    CONFIG = BalanceBlockData.CONFIG()
    # For dynamics assume pseudo-steady-state
    CONFIG.dynamic = False
    CONFIG.get("dynamic")._default = False
    CONFIG.get("dynamic")._domain = In([False])
    CONFIG.has_holdup = False
    CONFIG.get("has_holdup")._default = False
    CONFIG.get("has_holdup")._domain = In([False])
    # Rest of config to make this function like a turbine
    CONFIG.has_pressure_change = True
    CONFIG.get("has_pressure_change")._default = True
    CONFIG.get("has_pressure_change")._domain = In([True])
    CONFIG.has_work_transfer = True
    CONFIG.get("has_work_transfer")._default = True
    CONFIG.get("has_work_transfer")._domain = In([True])
    CONFIG.has_heat_transfer = False
    CONFIG.get("has_heat_transfer")._default = False
    CONFIG.get("has_heat_transfer")._domain = In([False])

    def build(self):
        """
        Add model equations to the unit model.  This is called by a default block
        construnction rule when the unit model is created.
        """
        super().build()  # Basic unit model build/read config
        config = self.config  # shorter config pointer

        # The thermodynamic expression writer object, te, writes expressions
        # including external function calls to calculate thermodynamic quantities
        # from a set of state variables.
        _assert_properties(config.property_package)
        te = ThermoExpr(blk=self, parameters=config.property_package)

        eff = self.efficiency_isentropic = pyo.Var(
            self.flowsheet().config.time,
            initialize=0.9,
            doc="Isentropic efficiency")
        eff.fix()

        pratio = self.ratioP = pyo.Var(self.flowsheet().config.time,
                                       initialize=0.7,
                                       doc="Ratio of outlet to inlet pressure")

        # Some shorter refernces to property blocks
        properties_in = self.control_volume.properties_in
        properties_out = self.control_volume.properties_out

        @self.Expression(self.flowsheet().config.time,
                         doc="Outlet isentropic enthalpy")
        def h_is(b, t):
            return te.h(s=properties_in[t].entr_mol,
                        p=properties_out[t].pressure)

        @self.Expression(self.flowsheet().config.time,
                         doc="Isentropic enthalpy change")
        def delta_enth_isentropic(b, t):
            return self.h_is[t] - properties_in[t].enth_mol

        @self.Expression(self.flowsheet().config.time, doc="Isentropic work")
        def work_isentropic(b, t):
            return properties_in[t].flow_mol * (properties_in[t].enth_mol -
                                                self.h_is[t])

        @self.Expression(self.flowsheet().config.time, doc="Outlet enthalpy")
        def h_o(b, t):  # Early access to the outlet enthalpy and work
            return properties_in[t].enth_mol - eff[t] * (
                properties_in[t].enth_mol - self.h_is[t])

        @self.Constraint(self.flowsheet().config.time)
        def eq_work(b, t):  # Work from energy balance
            return properties_out[t].enth_mol == self.h_o[t]

        @self.Constraint(self.flowsheet().config.time)
        def eq_pressure_ratio(b, t):
            return (pratio[t] *
                    properties_in[t].pressure == properties_out[t].pressure)

        @self.Expression(self.flowsheet().config.time)
        def work_mechanical(b, t):
            return b.control_volume.work[t]

    def _get_performance_contents(self, time_point=0):
        """This returns a dictionary of quntities to be used in IDAES unit model
        report generation routines.
        """
        pc = super()._get_performance_contents(time_point=time_point)
        return pc

    def initialize(
        self,
        outlvl=idaeslog.NOTSET,
        solver=None,
        optarg=None,
    ):
        """
        For simplicity this initialization requires you to set values for the
        efficency, inlet, and one of pressure ratio, pressure change or outlet
        pressure.
        """
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        # Create solver
        slvr = get_solver(solver, optarg)

        # Store original specification so initialization doesn't change the model
        # This will only resore the values of varaibles that were originally fixed
        sp = StoreSpec.value_isfixed_isactive(only_fixed=True)
        istate = to_json(self, return_dict=True, wts=sp)
        # Check for alternate pressure specs
        for t in self.flowsheet().config.time:
            if self.outlet.pressure[t].fixed:
                self.ratioP[t] = pyo.value(self.outlet.pressure[t] /
                                           self.inlet.pressure[t])
            elif self.control_volume.deltaP[t].fixed:
                self.ratioP[t] = pyo.value(
                    (self.control_volume.deltaP[t] + self.inlet.pressure[t]) /
                    self.inlet.pressure[t])
        # Fix the variables we base the initializtion on and free the rest.
        # This requires good values to be provided for pressure, efficency,
        # and inlet conditions, but it is simple and reliable.
        self.inlet.fix()
        self.outlet.unfix()
        self.ratioP.fix()
        self.deltaP.unfix()
        self.efficiency_isentropic.fix()
        for t in self.flowsheet().config.time:
            self.outlet.pressure[t] = pyo.value(self.inlet.pressure[t] *
                                                self.ratioP[t])
            self.deltaP[t] = pyo.value(self.outlet.pressure[t] -
                                       self.inlet.pressure[t])

            self.outlet.enth_mol[t] = pyo.value(self.h_o[t])
            self.control_volume.work[t] = pyo.value(
                self.inlet.flow_mol[t] * self.inlet.enth_mol[t] -
                self.outlet.flow_mol[t] * self.outlet.enth_mol[t])
            self.outlet.flow_mol[t] = pyo.value(self.inlet.flow_mol[t])
        # Solve the model (should be already solved from above)
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = slvr.solve(self, tee=slc.tee)
        from_json(self, sd=istate, wts=sp)

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

        for t, c in self.eq_pressure_ratio.items():
            s = iscale.get_scaling_factor(
                self.control_volume.properties_in[t].pressure)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
        for t, c in self.eq_work.items():
            s = iscale.get_scaling_factor(self.control_volume.work[t])
            iscale.constraint_scaling_transform(c, s, overwrite=False)
Esempio n. 25
0
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,
        domain=In(ConcentrationForm),
        description="Form to use for concentration terms in reaction equation",
        doc=
        "ConcentrationForm Enum indicating what form to use for concentration "
        "terms when constructing reaction equation."))
rxn_config.declare(
    "parameter_data",
    ConfigValue(
        default={},
        domain=dict,
        description="Dict containing initialization data for parameters"))

rate_rxn_config = rxn_config()
rate_rxn_config.declare(
    "rate_constant",
    ConfigValue(
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
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. 28
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)
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 Iapws95ParameterBlockData(PhysicalParameterBlock):
    CONFIG = PhysicalParameterBlock.CONFIG()

    CONFIG.declare(
        "phase_presentation",
        ConfigValue(
            default=PhaseType.MIX,
            domain=In(PhaseType),
            description="Set the way phases are presented to models",
            doc="""Set the way phases are presented to models. The MIX option
appears to the framework to be a mixed phase containing liquid and/or vapor.
The mixed option can simplify calculations at the unit model level since it can
be treated as a single phase, but unit models such as flash vessels will not be
able to treate the phases indepedently. The LG option presents as two sperate
phases to the framework. The L or G options can be used if it is known for sure
that only one phase is present.
**default** - PhaseType.MIX
**Valid values:** {
**PhaseType.MIX** - Present a mixed phase with liquid and/or vapor,
**PhaseType.LG** - Present a liquid and vapor phase,
**PhaseType.L** - Assume only liquid can be present,
**PhaseType.G** - Assume only vapor can be present}"""))

    CONFIG.declare(
        "state_vars",
        ConfigValue(
            default=StateVars.PH,
            domain=In(StateVars),
            description="State variable set",
            doc=
            """The set of state variables to use. Depending on the use, one state
variable set or another may be better computationally. Usually pressure and
enthalpy are the best choice because they are well behaved during a phase change.
**default** - StateVars.PH
**Valid values:** {
**StateVars.PH** - Pressure-Enthalpy,
**StateVars.TPX** - Temperature-Pressure-Quality}"""))

    def build(self):
        super(Iapws95ParameterBlockData, self).build()
        self.state_block_class = Iapws95StateBlock
        # Location of the *.so or *.dll file for external functions
        self.plib = _so
        self.available = os.path.isfile(self.plib)
        # Phase list
        self.private_phase_list = Set(initialize=["Vap", "Liq"])
        if self.config.phase_presentation == PhaseType.MIX:
            self.phase_list = Set(initialize=["Mix"])
        elif self.config.phase_presentation == PhaseType.LG:
            self.phase_list = Set(initialize=["Vap", "Liq"])
        elif self.config.phase_presentation == PhaseType.L:
            self.phase_list = Set(initialize=["Liq"])
        elif self.config.phase_presentation == PhaseType.G:
            self.phase_list = Set(initialize=["Vap"])
        # State var set
        self.state_vars = self.config.state_vars

        # Component list - a list of component identifiers
        self.component_list = Set(initialize=['H2O'])

        # List of phase equilibrium
        self.phase_equilibrium_idx = Set(initialize=[1])
        self.phase_equilibrium_list = {1: ["H2O", ("Vap", "Liq")]}

        # Parameters, these should match what's in the C code
        self.temperature_crit = Param(initialize=647.096,
                                      doc='Critical temperature [K]')
        self.pressure_crit = Param(initialize=2.2064e7,
                                   doc='Critical pressure [Pa]')
        self.dens_mass_crit = Param(initialize=322,
                                    doc='Critical density [kg/m3]')
        self.gas_const = Param(initialize=8.3144598,
                               doc='Gas Constant [J/mol/K]')
        self.mw = Param(initialize=0.01801528, doc='Molecular weight [kg/mol]')
        #Thermal conductivity parameters.
        # "Release on the IAPWS Formulation 2011 for the Thermal Conductivity of
        # Ordinary Water Substance"
        self.tc_L0 = Param(RangeSet(0, 5),
                           initialize={
                               0: 2.443221e-3,
                               1: 1.323095e-2,
                               2: 6.770357e-3,
                               3: -3.454586e-3,
                               4: 4.096266e-4
                           },
                           doc="0th order themalcondutivity paramters")

        self.tc_L1 = Param(RangeSet(0, 5),
                           RangeSet(0, 6),
                           initialize={
                               (0, 0): 1.60397357,
                               (1, 0): 2.33771842,
                               (2, 0): 2.19650529,
                               (3, 0): -1.21051378,
                               (4, 0): -2.7203370,
                               (0, 1): -0.646013523,
                               (1, 1): -2.78843778,
                               (2, 1): -4.54580785,
                               (3, 1): 1.60812989,
                               (4, 1): 4.57586331,
                               (0, 2): 0.111443906,
                               (1, 2): 1.53616167,
                               (2, 2): 3.55777244,
                               (3, 2): -0.621178141,
                               (4, 2): -3.18369245,
                               (0, 3): 0.102997357,
                               (1, 3): -0.463045512,
                               (2, 3): -1.40944978,
                               (3, 3): 0.0716373224,
                               (4, 3): 1.1168348,
                               (0, 4): -0.0504123634,
                               (1, 4): 0.0832827019,
                               (2, 4): 0.275418278,
                               (3, 4): 0.0,
                               (4, 4): -0.19268305,
                               (0, 5): 0.00609859258,
                               (1, 5): -0.00719201245,
                               (2, 5): -0.0205938816,
                               (3, 5): 0.0,
                               (4, 5): 0.012913842
                           },
                           doc="1st order themalcondutivity paramters")
        #Viscosity paramters
        #"Release on the IAPWS Formulation 2008 for the Viscosity of
        # Ordinary Water Substance "
        self.visc_H0 = Param(RangeSet(0, 4),
                             initialize={
                                 0: 1.67752,
                                 1: 2.20462,
                                 2: 0.6366564,
                                 3: -0.241605
                             },
                             doc="0th order viscosity parameters")

        self.visc_H1 = Param(RangeSet(0, 6),
                             RangeSet(0, 7),
                             initialize={
                                 (0, 0): 5.20094e-1,
                                 (1, 0): 8.50895e-2,
                                 (2, 0): -1.08374,
                                 (3, 0): -2.89555e-1,
                                 (4, 0): 0.0,
                                 (5, 0): 0.0,
                                 (0, 1): 2.22531e-1,
                                 (1, 1): 9.99115e-1,
                                 (2, 1): 1.88797,
                                 (3, 1): 1.26613,
                                 (4, 1): 0.0,
                                 (5, 1): 1.20573e-1,
                                 (0, 2): -2.81378e-1,
                                 (1, 2): -9.06851e-1,
                                 (2, 2): -7.72479e-1,
                                 (3, 2): -4.89837e-1,
                                 (4, 2): -2.57040e-1,
                                 (5, 2): 0.0,
                                 (0, 3): 1.61913e-1,
                                 (1, 3): 2.57399e-1,
                                 (2, 3): 0.0,
                                 (3, 3): 0.0,
                                 (4, 3): 0.0,
                                 (5, 3): 0.0,
                                 (0, 4): -3.25372e-2,
                                 (1, 4): 0.0,
                                 (2, 4): 0.0,
                                 (3, 4): 6.98452e-2,
                                 (4, 4): 0.0,
                                 (5, 4): 0.0,
                                 (0, 5): 0.0,
                                 (1, 5): 0.0,
                                 (2, 5): 0.0,
                                 (3, 5): 0.0,
                                 (4, 5): 8.72102e-3,
                                 (5, 5): 0.0,
                                 (0, 6): 0.0,
                                 (1, 6): 0.0,
                                 (2, 6): 0.0,
                                 (3, 6): -4.35673e-3,
                                 (4, 6): 0.0,
                                 (5, 6): -5.93264e-4
                             },
                             doc="1st order viscosity parameters")

        self.smoothing_pressure_over = Param(
            mutable=True,
            initialize=1e-4,
            doc='Smooth max parameter (pressure over)')
        self.smoothing_pressure_under = Param(
            mutable=True,
            initialize=1e-4,
            doc='Smooth max parameter (pressure under)')

    @classmethod
    def define_metadata(cls, obj):
        obj.add_properties({
            'temperature_crit': {
                'method': None,
                'units': 'K'
            },
            'pressure_crit': {
                'method': None,
                'units': 'Pa'
            },
            'dens_mass_crit': {
                'method': None,
                'units': 'kg/m^3'
            },
            'gas_const': {
                'method': None,
                'units': 'J/mol.K'
            },
            'mw': {
                'method': None,
                'units': 'kg/mol'
            },
            'temperature_sat': {
                'method': 'None',
                'units': 'K'
            },
            'flow_mol': {
                'method': None,
                'units': 'mol/s'
            },
            'flow_mass': {
                'method': None,
                'units': 'kg/s'
            },
            'temperature': {
                'method': None,
                'units': 'K'
            },
            'pressure': {
                'method': None,
                'units': 'Pa'
            },
            'vapor_frac': {
                'method': None,
                'units': None
            },
            'dens_mass_phase': {
                'method': None,
                'units': 'kg/m^3'
            },
            'temperature_red': {
                'method': None,
                'units': None
            },
            'pressure_sat': {
                'method': None,
                'units': 'kPa'
            },
            'energy_internal_mol_phase': {
                'method': None,
                'units': 'J/mol'
            },
            'enth_mol_phase': {
                'method': None,
                'units': 'J/mol'
            },
            'entr_mol_phase': {
                'method': None,
                'units': 'J/mol.K'
            },
            'cp_mol_phase': {
                'method': None,
                'units': 'J/mol.K'
            },
            'cv_mol_phase': {
                'method': None,
                'units': 'J/mol.K'
            },
            'speed_sound_phase': {
                'method': None,
                'units': 'm/s'
            },
            'dens_mol_phase': {
                'method': None,
                'units': 'mol/m^3'
            },
            'therm_cond_phase': {
                'method': None,
                'units': 'W/m.K'
            },
            'visc_d_phase': {
                'method': None,
                'units': 'Pa.s'
            },
            'visc_k_phase': {
                'method': None,
                'units': 'm^2/s'
            },
            'phase_frac': {
                'method': None,
                'units': None
            },
            'flow_mol_comp': {
                'method': None,
                'units': 'mol/s'
            },
            'energy_internal_mol': {
                'method': None,
                'units': 'J/mol'
            },
            'enth_mol': {
                'method': None,
                'units': 'J/mol'
            },
            'entr_mol': {
                'method': None,
                'units': 'J/mol.K'
            },
            'cp_mol': {
                'method': None,
                'units': 'J/mol.K'
            },
            'cv_mol': {
                'method': None,
                'units': 'J/mol.K'
            },
            'heat_capacity_ratio': {
                'method': None,
                'units': None
            },
            'dens_mass': {
                'method': None,
                'units': 'kg/m^3'
            },
            'dens_mol': {
                'method': None,
                'units': 'mol/m^3'
            },
            'dh_vap_mol': {
                'method': None,
                'units': 'J/mol'
            }
        })

        obj.add_default_units({
            'time': 's',
            'length': 'm',
            'mass': 'kg',
            'amount': 'mol',
            'temperature': 'K',
            'energy': 'J',
            'holdup': 'mol'
        })