Exemple #1
0
class TrayData(UnitModelBlockData):
    """
    Tray unit for distillation 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. Flash units 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. Flash units do not have defined volume, thus
this must be False."""))
    CONFIG.declare(
        "is_feed_tray",
        ConfigValue(default=False,
                    domain=Bool,
                    description="flag to indicate feed tray.",
                    doc="""indicates if this is a feed tray and constructs
corresponding ports,
**default** - False.
**Valid values:** {
**True** - feed tray,
**False** - conventional tray with no feed inlet}"""))
    CONFIG.declare(
        "has_liquid_side_draw",
        ConfigValue(
            default=False,
            domain=Bool,
            description="liquid side draw construction flag.",
            doc="""indicates if there is a liquid side draw from the tray,
**default** - False.
**Valid values:** {
**True** - include a liquid side draw from the tray,
**False** - exclude a liquid side draw from the tray.}"""))
    CONFIG.declare(
        "has_vapor_side_draw",
        ConfigValue(
            default=False,
            domain=Bool,
            description="vapor side draw construction flag.",
            doc="""indicates if there is a vapor side draw from the tray,
**default** - False.
**Valid values:** {
**True** - include a vapor side draw from the tray,
**False** - exclude a vapor side draw from the tray.}"""))
    CONFIG.declare(
        "has_heat_transfer",
        ConfigValue(default=False,
                    domain=Bool,
                    description="heat duty to/from tray construction flag.",
                    doc="""indicates if there is heat duty to/from the tray,
**default** - False.
**Valid values:** {
**True** - include a heat duty term,
**False** - exclude a heat duty term.}"""))
    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.}"""))

    def build(self):
        """Build the model.

        Args:
            None
        Returns:
            None
        """
        # Setup model build logger
        model_log = idaeslog.getModelLogger(self.name, tag="unit")

        # Call UnitModel.build to setup dynamics
        super(TrayData, self).build()

        # Create the inlet list to build inlet state blocks
        if self.config.is_feed_tray:
            inlet_list = ["feed", "liq", "vap"]
        else:
            inlet_list = ["liq", "vap"]

        # Create a dict to set up the inlet state blocks
        state_block_args = dict(**self.config.property_package_args)
        state_block_args["has_phase_equilibrium"] = True
        state_block_args["defined_state"] = True

        for i in inlet_list:
            state_obj = self.config.property_package.build_state_block(
                self.flowsheet().time,
                doc="State block for " + i + "_inlet to tray",
                default=state_block_args)

            setattr(self, "properties_in_" + i, state_obj)

        # Create a dict to set up the mixed outlet state blocks
        mixed_block_args = dict(**self.config.property_package_args)
        mixed_block_args["has_phase_equilibrium"] = True
        mixed_block_args["defined_state"] = False

        self.properties_out = self.config.property_package.\
            build_state_block(self.flowsheet().time,
                              doc="State block for mixed outlet from tray",
                              default=mixed_block_args)

        self._add_material_balance()
        self._add_energy_balance()
        self._add_pressure_balance()

        # Get liquid and vapor phase objects from the property package
        # to be used below. Avoids repition.
        _liquid_list = []
        _vapor_list = []
        for p in self.config.property_package.phase_list:
            pobj = self.config.property_package.get_phase(p)
            if pobj.is_vapor_phase():
                _vapor_list.append(p)
            elif pobj.is_liquid_phase():
                _liquid_list.append(p)
            else:
                _liquid_list.append(p)
                model_log.warning(
                    "A non-liquid/non-vapor phase was detected but will "
                    "be treated as a liquid.")

        # Create a pyomo set for indexing purposes. This set is appended to
        # model otherwise results in an abstract set.
        self._liquid_set = Set(initialize=_liquid_list)
        self._vapor_set = Set(initialize=_vapor_list)

        self._add_ports()

    def _add_material_balance(self):
        """Method to construct the mass balance equation."""
        @self.Constraint(self.flowsheet().time,
                         self.config.property_package.component_list,
                         doc="material balance")
        def material_mixing_equations(b, t, j):
            if self.config.is_feed_tray:
                return 0 == sum(
                    self.properties_in_feed[t].get_material_flow_terms(p, j) +
                    self.properties_in_liq[t].get_material_flow_terms(p, j) +
                    self.properties_in_vap[t].get_material_flow_terms(p, j) -
                    self.properties_out[t].get_material_flow_terms(p, j)
                    for p in b.config.property_package.phase_list)
            else:
                return 0 == sum(
                    self.properties_in_liq[t].get_material_flow_terms(p, j) +
                    self.properties_in_vap[t].get_material_flow_terms(p, j) -
                    self.properties_out[t].get_material_flow_terms(p, j)
                    for p in b.config.property_package.phase_list)

    def _add_energy_balance(self):
        """Method to construct the energy balance equation."""

        if self.config.has_heat_transfer:
            units_meta = self.config.property_package.get_metadata()
            self.heat_duty = Var(self.flowsheet().time,
                                 initialize=0,
                                 doc="Heat duty for the tray",
                                 units=units_meta.get_derived_units("power"))

        @self.Constraint(self.flowsheet().time, doc="energy balance")
        def enthalpy_mixing_equations(b, t):
            if self.config.is_feed_tray:
                if self.config.has_heat_transfer:
                    return 0 == (
                        sum(self.properties_in_feed[t].
                            get_enthalpy_flow_terms(p)
                            for p in b.config.property_package.phase_list) +
                        sum(self.properties_in_liq[t].
                            get_enthalpy_flow_terms(p)
                            for p in b.config.property_package.phase_list) +
                        sum(self.properties_in_vap[t].
                            get_enthalpy_flow_terms(p)
                            for p in b.config.property_package.phase_list) -
                        sum(self.properties_out[t].
                            get_enthalpy_flow_terms(p)
                            for p in b.config.property_package.phase_list)) + \
                        self.heat_duty[t]
                else:
                    return 0 == (
                        sum(self.properties_in_feed[t].get_enthalpy_flow_terms(
                            p) for p in b.config.property_package.phase_list) +
                        sum(self.properties_in_liq[t].get_enthalpy_flow_terms(
                            p) for p in b.config.property_package.phase_list) +
                        sum(self.properties_in_vap[t].get_enthalpy_flow_terms(
                            p) for p in b.config.property_package.phase_list) -
                        sum(self.properties_out[t].get_enthalpy_flow_terms(p)
                            for p in b.config.property_package.phase_list))
            else:
                if self.config.has_heat_transfer:
                    return 0 == (
                        sum(self.properties_in_liq[t].
                            get_enthalpy_flow_terms(p)
                            for p in b.config.property_package.phase_list) +
                        sum(self.properties_in_vap[t].
                            get_enthalpy_flow_terms(p)
                            for p in b.config.property_package.phase_list) -
                        sum(self.properties_out[t].
                            get_enthalpy_flow_terms(p)
                            for p in b.config.property_package.phase_list)) + \
                        self.heat_duty[t]
                else:
                    return 0 == (sum(
                        self.properties_in_liq[t].get_enthalpy_flow_terms(p)
                        for p in b.config.property_package.phase_list
                    ) + sum(
                        self.properties_in_vap[t].get_enthalpy_flow_terms(p)
                        for p in b.config.property_package.phase_list) - sum(
                            self.properties_out[t].get_enthalpy_flow_terms(p)
                            for p in b.config.property_package.phase_list))

    def _add_pressure_balance(self):
        """Method to construct the pressure balance."""
        if self.config.has_pressure_change:
            units_meta = self.config.property_package.get_metadata()
            self.deltaP = Var(self.flowsheet().time,
                              initialize=0,
                              doc="Pressure drop across tray",
                              units=units_meta.get_derived_units("pressure"))

        @self.Constraint(self.flowsheet().time,
                         doc="pressure balance for tray")
        def pressure_drop_equation(self, t):
            if self.config.has_pressure_change:
                return self.properties_out[t].pressure == \
                    self.properties_in_liq[t].pressure - self.deltaP[t]
            else:
                return self.properties_out[t].pressure == \
                    self.properties_in_liq[t].pressure

    def _add_ports(self):
        """Method to construct the ports for the tray."""

        # Add feed inlet port
        if self.config.is_feed_tray:
            self.add_inlet_port(name="feed", block=self.properties_in_feed)

        # Add liquid and vapor inlet ports
        self.add_inlet_port(name="liq_in", block=self.properties_in_liq)
        self.add_inlet_port(name="vap_in", block=self.properties_in_vap)

        # Add liquid outlet port
        self.liq_out = Port(noruleinit=True, doc="liquid outlet from tray")

        # Add liquid side draw port if selected
        if self.config.has_liquid_side_draw:
            self.liq_side_sf = Var(
                initialize=0.01, doc="split fraction for the liquid side draw")
            self.liq_side_draw = Port(noruleinit=True, doc="liquid side draw.")
            self._make_phase_split(
                port=self.liq_side_draw,
                phase=self._liquid_set,
                has_liquid_side_draw=self.config.has_liquid_side_draw,
                side_sf=self.liq_side_sf)

            # Populate the liquid outlet port with the remaining liquid
            # after the side draw
            self._make_phase_split(port=self.liq_out,
                                   phase=self._liquid_set,
                                   side_sf=1 - self.liq_side_sf)
        else:
            # Populate the liquid outlet port when no liquid side draw
            self._make_phase_split(port=self.liq_out,
                                   phase=self._liquid_set,
                                   side_sf=1)

        # Add the vapor outlet port
        self.vap_out = Port(noruleinit=True, doc="vapor outlet from tray")

        # Add vapor side draw port if selected
        if self.config.has_vapor_side_draw:
            self.vap_side_sf = Var(
                initialize=0.01, doc="split fraction for the vapor side draw")
            self.vap_side_draw = Port(noruleinit=True, doc="vapor side draw.")
            self._make_phase_split(
                port=self.vap_side_draw,
                phase=self._vapor_set,
                has_vapor_side_draw=self.config.has_vapor_side_draw,
                side_sf=self.vap_side_sf)
            # Populate the vapor outlet port with the remaining vapor
            # after the vapor side draw
            self._make_phase_split(port=self.vap_out,
                                   phase=self._vapor_set,
                                   side_sf=1 - self.vap_side_sf)
        else:
            # Populate the vapor outlet port when no vapor side draw
            self._make_phase_split(port=self.vap_out,
                                   phase=self._vapor_set,
                                   side_sf=1)

    def _make_phase_split(self,
                          port=None,
                          phase=None,
                          has_liquid_side_draw=False,
                          has_vapor_side_draw=False,
                          side_sf=None):
        """Method to split and populate the outlet ports with corresponding
           phase values from the mixed stream outlet block."""

        member_list = self.properties_out[0].define_port_members()

        for k in member_list:

            local_name = member_list[k].local_name

            # Create references and populate the intensive variables
            if "flow" not in local_name and "frac" not in local_name \
                    and "enth" not in local_name:
                if not member_list[k].is_indexed():
                    var = self.properties_out[:].\
                        component(local_name)
                else:
                    var = self.properties_out[:].\
                        component(local_name)[...]

                # add the reference and variable name to the port
                ref = Reference(var)
                setattr(self, "_" + k + "_ref", ref)
                port.add(ref, k)

            elif "frac" in local_name:

                # Mole/mass frac is typically indexed
                index_set = member_list[k].index_set()

                # if state var is not mole/mass frac by phase
                if "phase" not in local_name:
                    if "mole" in local_name:  # check mole basis/mass basis

                        # The following conditionals are required when a
                        # mole frac or mass frac is a state var i.e. will be
                        # a port member. This gets a bit tricky when handling
                        # non-conventional systems when you have more than one
                        # liquid or vapor phase. Hence, the logic here is that
                        # the mole frac that should be present in the liquid or
                        # vapor port should be computed by accounting for
                        # multiple liquid or vapor phases if present. For the
                        # classical VLE system, this holds too.
                        if hasattr(self.properties_out[0],
                                   "mole_frac_phase_comp") and \
                            hasattr(self.properties_out[0],
                                    "flow_mol_phase"):
                            flow_phase_comp = False
                            local_name_frac = "mole_frac_phase_comp"
                            local_name_flow = "flow_mol_phase"
                        elif hasattr(self.properties_out[0],
                                     "flow_mol_phase_comp"):
                            flow_phase_comp = True
                            local_name_flow = "flow_mol_phase_comp"
                        else:
                            raise PropertyNotSupportedError(
                                "No mole_frac_phase_comp or flow_mol_phase or"
                                " flow_mol_phase_comp variables encountered "
                                "while building ports for the condenser. ")
                    elif "mass" in local_name:
                        if hasattr(self.properties_out[0],
                                   "mass_frac_phase_comp") and \
                            hasattr(self.properties_out[0],
                                    "flow_mass_phase"):
                            flow_phase_comp = False
                            local_name_frac = "mass_frac_phase_comp"
                            local_name_flow = "flow_mass_phase"
                        elif hasattr(self.properties_out[0],
                                     "flow_mass_phase_comp"):
                            flow_phase_comp = True
                            local_name_flow = "flow_mass_phase_comp"
                        else:
                            raise PropertyNotSupportedError(
                                "No mass_frac_phase_comp or flow_mass_phase or"
                                " flow_mass_phase_comp variables encountered "
                                "while building ports for the condenser.")
                    else:
                        raise PropertyNotSupportedError(
                            "No mass frac or mole frac variables encountered "
                            " while building ports for the condenser. "
                            "phase_frac as a state variable is not "
                            "supported with distillation unit models.")

                    # Rule for mole fraction
                    def rule_mole_frac(self, t, i):
                        if not flow_phase_comp:
                            sum_flow_comp = sum(
                                self.properties_out[t].component(
                                    local_name_frac)[p, i] *
                                self.properties_out[t].component(
                                    local_name_flow)[p] for p in phase)

                            return sum_flow_comp / sum(
                                self.properties_out[t].component(
                                    local_name_flow)[p] for p in phase)
                        else:
                            sum_flow_comp = sum(
                                self.properties_out[t].component(
                                    local_name_flow)[p, i] for p in phase)

                            return sum_flow_comp / sum(
                                self.properties_out[t].component(
                                    local_name_flow)[p, i] for p in phase for i
                                in self.config.property_package.component_list)

                    # add the reference and variable name to the port
                    expr = Expression(self.flowsheet().time,
                                      index_set,
                                      rule=rule_mole_frac)
                    self.add_component("e_mole_frac_" + port.local_name, expr)
                    port.add(expr, k)
                else:

                    # Assumes mole_frac_phase or mass_frac_phase exist as
                    # state vars in the port and therefore access directly
                    # from the state block.
                    var = self.properties_out[:].\
                        component(local_name)[...]

                    # add the reference and variable name to the port
                    ref = Reference(var)
                    setattr(self, "_" + k + "_" + port.local_name + "_ref",
                            ref)
                    port.add(ref, k)
            elif "flow" in local_name:
                if "phase" not in local_name:

                    # Assumes that here the var is total flow or component
                    # flow. However, need to extract the flow by phase from
                    # the state block. Expects to find the var
                    # flow_mol_phase or flow_mass_phase in the state block.

                    # Check if it is not indexed by component list and this
                    # is total flow
                    if not member_list[k].is_indexed():
                        # if state var is not flow_mol/flow_mass
                        # by phase
                        local_name_flow = local_name + "_phase"

                        # Rule to link the flow to the port
                        def rule_flow(self, t):
                            return sum(self.properties_out[t].component(
                                local_name_flow)[p] for p in phase) * (side_sf)

                        # add the reference and variable name to the port
                        expr = Expression(self.flowsheet().time,
                                          rule=rule_flow)
                        self.add_component("e_flow_" + port.local_name, expr)
                        port.add(expr, k)
                    else:
                        # when it is flow comp indexed by component list
                        str_split = local_name.split("_")
                        if len(str_split) == 3 and str_split[-1] == "comp":
                            local_name_flow = str_split[0] + "_" + \
                                str_split[1] + "_phase_" + "comp"

                        # Get the indexing set i.e. component list
                        index_set = member_list[k].index_set()

                        # Rule to link the flow to the port
                        def rule_flow(self, t, i):
                            return sum(self.properties_out[t].component(
                                local_name_flow)[p, i]
                                       for p in phase) * (side_sf)

                        expr = Expression(self.flowsheet().time,
                                          index_set,
                                          rule=rule_flow)
                        self.add_component("e_flow_" + port.local_name, expr)
                        port.add(expr, local_name)
                elif "phase" in local_name:
                    # flow is indexed by phase and comp
                    # Get the indexing sets i.e. component list and phase list
                    component_set = self.config.\
                        property_package.component_list

                    phase_set = self.config.\
                        property_package.phase_list

                    def rule_flow(self, t, p, i):
                        if (phase is self._liquid_set and
                                p in self._liquid_set) or \
                                (phase is self._vapor_set and
                                 p in self._vapor_set) :
                            # pass appropriate phase flow values to port
                            return (self.properties_out[t].component(
                                local_name)[p, i]) * (side_sf)
                        else:
                            # return small number for phase that should not
                            # be in the appropriate port. For example,
                            # the state vars will be flow_mol_phase_comp
                            # which will include all phases. The liq port
                            # should have the correct references to the liq
                            # phase flow but the vapor phase flow should be 0.
                            return 1e-8

                    expr = Expression(self.flowsheet().time,
                                      phase_set,
                                      component_set,
                                      rule=rule_flow)
                    self.add_component("e_" + local_name + port.local_name,
                                       expr)
                    port.add(expr, k)
                else:
                    raise PropertyPackageError(
                        "Unrecognized flow state variable encountered "
                        "while building ports for the tray. Please follow "
                        "the naming convention outlined in the documentation "
                        "for state variables.")
            elif "enth" in local_name:
                if "phase" not in local_name:
                    # assumes total mixture enthalpy (enth_mol or enth_mass)
                    if not member_list[k].is_indexed():
                        # if state var is not enth_mol/enth_mass
                        # by phase, add _phase string to extract the right
                        # value from the state block
                        local_name_phase = local_name + "_phase"
                    else:
                        raise PropertyPackageError(
                            "Enthalpy is indexed but the variable "
                            "name does not reflect the presence of an index. "
                            "Please follow the naming convention outlined "
                            "in the documentation for state variables.")

                    # Rule to link the phase enthalpy to the port.
                    def rule_enth(self, t):
                        return sum(self.properties_out[t].component(
                            local_name_phase)[p] for p in phase)

                    expr = Expression(self.flowsheet().time, rule=rule_enth)
                    self.add_component("e_enth_" + port.local_name, expr)
                    # add the reference and variable name to the port
                    port.add(expr, k)

                elif "phase" in local_name:
                    # assumes enth_mol_phase or enth_mass_phase.
                    # This is an intensive property, you create a direct
                    # reference irrespective of the reflux, distillate and
                    # vap_outlet

                    if not member_list[k].is_indexed():
                        var = self.properties_out[:].\
                            component(local_name)
                    else:
                        var = self.properties_out[:].\
                            component(local_name)[...]

                    # add the reference and variable name to the port
                    ref = Reference(var)
                    setattr(self, "_" + k + "_" + port.local_name + "_ref",
                            ref)
                    port.add(ref, k)
                else:
                    raise PropertyNotSupportedError(
                        "Unrecognized enthalpy state variable encountered "
                        "while building ports for the tray. Only total "
                        "mixture enthalpy or enthalpy by phase are supported.")

    def initialize(self,
                   state_args_feed=None,
                   state_args_liq=None,
                   state_args_vap=None,
                   hold_state_liq=False,
                   hold_state_vap=False,
                   solver=None,
                   optarg=None,
                   outlvl=idaeslog.NOTSET):

        # TODO:
        # 1. Initialization for dynamic mode. Currently not supported.
        # 2. Handle unfixed side split fraction vars
        # 3. Better logic to handle and fix state vars.

        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        init_log.info("Begin initialization.")

        solverobj = get_solver(solver, optarg)

        if self.config.has_liquid_side_draw:
            if not self.liq_side_sf.fixed:
                raise ConfigurationError(
                    "Liquid side draw split fraction not fixed but "
                    "has_liquid_side_draw set to True.")

        if self.config.has_vapor_side_draw:
            if not self.vap_side_sf.fixed:
                raise ConfigurationError(
                    "Vapor side draw split fraction not fixed but "
                    "has_vapor_side_draw set to True.")

        # Create initial guess if not provided by using current values
        if self.config.is_feed_tray and state_args_feed is None:
            state_args_feed = {}
            state_args_liq = {}
            state_args_vap = {}
            state_dict = (self.properties_in_feed[
                self.flowsheet().time.first()].define_port_members())

            for k in state_dict.keys():
                if "flow" in k:
                    if state_dict[k].is_indexed():
                        state_args_feed[k] = {}
                        state_args_liq[k] = {}
                        state_args_vap[k] = {}
                        for m in state_dict[k].keys():
                            state_args_feed[k][m] = \
                                value(state_dict[k][m])
                            state_args_liq[k][m] = \
                                value(0.1 * state_dict[k][m])
                            state_args_vap[k][m] = \
                                value(0.1 * state_dict[k][m])

                    else:
                        state_args_feed[k] = value(state_dict[k])
                        state_args_liq[k] = 0.1 * value(state_dict[k])
                        state_args_vap[k] = 0.1 * value(state_dict[k])
                else:
                    if state_dict[k].is_indexed():
                        state_args_feed[k] = {}
                        state_args_liq[k] = {}
                        state_args_vap[k] = {}
                        for m in state_dict[k].keys():
                            state_args_feed[k][m] = \
                                value(state_dict[k][m])
                            state_args_liq[k][m] = \
                                value(state_dict[k][m])
                            state_args_vap[k][m] = \
                                value(state_dict[k][m])

                    else:
                        state_args_feed[k] = value(state_dict[k])
                        state_args_liq[k] = value(state_dict[k])
                        state_args_vap[k] = value(state_dict[k])

        # Create initial guess if not provided by using current values
        if not self.config.is_feed_tray and state_args_liq is None:
            state_args_liq = {}
            state_dict = (self.properties_in_liq[
                self.flowsheet().time.first()].define_port_members())

            for k in state_dict.keys():
                if state_dict[k].is_indexed():
                    state_args_liq[k] = {}
                    for m in state_dict[k].keys():
                        state_args_liq[k][m] = \
                            value(state_dict[k][m])
                else:
                    state_args_liq[k] = value(state_dict[k])

        # Create initial guess if not provided by using current values
        if not self.config.is_feed_tray and state_args_vap is None:
            state_args_vap = {}
            state_dict = (self.properties_in_vap[
                self.flowsheet().time.first()].define_port_members())

            for k in state_dict.keys():
                if state_dict[k].is_indexed():
                    state_args_vap[k] = {}
                    for m in state_dict[k].keys():
                        state_args_vap[k][m] = \
                            value(state_dict[k][m])
                else:
                    state_args_vap[k] = value(state_dict[k])

        if self.config.is_feed_tray:
            feed_flags = self.properties_in_feed.initialize(
                outlvl=outlvl,
                solver=solver,
                optarg=optarg,
                hold_state=True,
                state_args=state_args_feed,
                state_vars_fixed=False)

        liq_in_flags = self.properties_in_liq. \
            initialize(outlvl=outlvl,
                       solver=solver,
                       optarg=optarg,
                       hold_state=True,
                       state_args=state_args_liq,
                       state_vars_fixed=False)

        vap_in_flags = self.properties_in_vap. \
            initialize(outlvl=outlvl,
                       solver=solver,
                       optarg=optarg,
                       hold_state=True,
                       state_args=state_args_vap,
                       state_vars_fixed=False)

        # state args to initialize the mixed outlet state block
        state_args_mixed = {}

        if self.config.is_feed_tray:

            # if feed tray, initialize the mixed state block at
            # the same condition.
            state_args_mixed = state_args_feed
        else:
            # if not feed tray, initialize mixed state block at average of
            # vap/liq inlets except pressure. While this is crude, it
            # will work for most combination of state vars.
            state_dict = (self.properties_in_liq[
                self.flowsheet().time.first()].define_port_members())
            for k in state_dict.keys():
                if k == "pressure":
                    # Take the lowest pressure and this is the liq inlet
                    state_args_mixed[k] = value(
                        self.properties_in_liq[0].component(
                            state_dict[k].local_name))
                elif state_dict[k].is_indexed():
                    state_args_mixed[k] = {}
                    for m in state_dict[k].keys():
                        if "flow" in k:
                            state_args_mixed[k][m] = \
                                value(self.properties_in_liq[0].
                                      component(state_dict[k].local_name)[m]) \
                                + value(self.properties_in_vap[0].
                                        component(state_dict[k].local_name)[m])

                        else:
                            state_args_mixed[k][m] = \
                                0.5 * (value(self.properties_in_liq[0].
                                             component(state_dict[k].
                                                       local_name)[m]) +
                                       value(self.properties_in_vap[0].
                                       component(state_dict[k].local_name)[m]))

                else:
                    if "flow" in k:
                        state_args_mixed[k] = \
                            value(self.properties_in_liq[0].
                                  component(state_dict[k].local_name)) +\
                            value(self.properties_in_vap[0].
                                  component(state_dict[k].local_name))
                    else:
                        state_args_mixed[k] = \
                            0.5 * (value(self.properties_in_liq[0].
                                         component(state_dict[k].local_name)) +
                                   value(self.properties_in_vap[0].
                                         component(state_dict[k].local_name)))

        # Initialize the mixed outlet state block
        self.properties_out. \
            initialize(outlvl=outlvl,
                       solver=solver,
                       optarg=optarg,
                       hold_state=False,
                       state_args=state_args_mixed,
                       state_vars_fixed=False)

        # Deactivate energy balance
        self.enthalpy_mixing_equations.deactivate()

        # Try fixing the outlet temperature if else pass
        # NOTE: if passed then there would probably be a degree of freedom
        try:
            self.properties_out[:].temperature.\
                fix(state_args_mixed["temperature"])
        except AttributeError:
            init_log.warning("Trying to fix outlet temperature "
                             "during initialization but temperature attribute "
                             "unavailable in the state block. Initialization "
                             "proceeding with a potential degree of freedom.")

        # Deactivate pressure balance
        self.pressure_drop_equation.deactivate()

        # Try fixing the outlet temperature if else pass
        # NOTE: if passed then there would probably be a degree of freedom
        try:
            self.properties_out[:].pressure.\
                fix(state_args_mixed["pressure"])
        except AttributeError:
            init_log.warning("Trying to fix outlet pressure "
                             "during initialization but pressure attribute "
                             "unavailable in the state block. Initialization "
                             "proceeding with a potential degree of freedom.")

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = solverobj.solve(self, tee=slc.tee)
        init_log.info("Mass balance solve {}.".format(idaeslog.condition(res)))

        # Activate energy balance
        self.enthalpy_mixing_equations.activate()
        try:
            self.properties_out[:].temperature.unfix()
        except AttributeError:
            pass

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = solverobj.solve(self, tee=slc.tee)
        init_log.info("Mass and energy balance solve {}.".format(
            idaeslog.condition(res)))

        # Activate pressure balance
        self.pressure_drop_equation.activate()
        try:
            self.properties_out[:].pressure.unfix()
        except AttributeError:
            pass

        if degrees_of_freedom(self) == 0:
            with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
                res = solverobj.solve(self, tee=slc.tee)
            init_log.info("Mass, energy and pressure balance solve {}.".format(
                idaeslog.condition(res)))
        else:
            raise Exception("State vars fixed but degrees of freedom "
                            "for tray block is not zero during "
                            "initialization.")

        if not check_optimal_termination(res):
            raise InitializationError(
                f"{self.name} failed to initialize successfully. Please check "
                f"the output logs for more information.")

        init_log.info("Initialization complete, status {}.".format(
            idaeslog.condition(res)))

        if not self.config.is_feed_tray:
            if not hold_state_vap:
                self.properties_in_vap.release_state(flags=vap_in_flags,
                                                     outlvl=outlvl)
            if not hold_state_liq:
                self.properties_in_liq.release_state(flags=liq_in_flags,
                                                     outlvl=outlvl)
            if hold_state_liq and hold_state_vap:
                return liq_in_flags, vap_in_flags
            elif hold_state_vap:
                return vap_in_flags
            elif hold_state_liq:
                return liq_in_flags
        else:
            self.properties_in_liq.release_state(flags=liq_in_flags,
                                                 outlvl=outlvl)
            self.properties_in_vap.release_state(flags=vap_in_flags,
                                                 outlvl=outlvl)
            return feed_flags
Exemple #2
0
def _define_turbine_multistage_config(config):
    config.declare(
        "dynamic",
        ConfigValue(
            domain=In([False]),
            default=False,
            description="Dynamic model flag",
            doc=
            "Only False, in a dynamic flowsheet this is psuedo-steady-state.",
        ),
    )
    config.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=In([False]),
            description="Holdup construction flag",
            doc=
            "Only False, in a dynamic flowsheet this is psuedo-steady-state.",
        ),
    )
    config.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
        ),
    )
    config.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
        ),
    )
    config.declare(
        "num_parallel_inlet_stages",
        ConfigValue(
            default=4,
            domain=int,
            description=
            "Number of parallel inlet stages to simulate partial arc "
            "admission.  Default=4",
        ),
    )
    config.declare(
        "throttle_valve_function",
        ConfigValue(
            default=ValveFunctionType.linear,
            domain=In(ValveFunctionType),
            description=
            "Valve function type, if custom provide an expression rule",
            doc=
            """The type of valve function, if custom provide an expression rule
with the valve_function_rule argument.
**default** - ValveFunctionType.linear
**Valid values** - {
ValveFunctionType.linear,
ValveFunctionType.quick_opening,
ValveFunctionType.equal_percentage,
ValveFunctionType.custom}""",
        ),
    )
    config.declare(
        "throttle_valve_function_callback",
        ConfigValue(
            default=None,
            description="A callback to add a custom valve function to the "
            "throttle valves or None.  If a callback is provided, it should "
            "take the valve block data as an argument and add a "
            "valve_function expressions to it. Default=None",
        ),
    )
    config.declare(
        "num_hp",
        ConfigValue(
            default=2,
            domain=int,
            description=
            "Number of high pressure stages not including inlet stage",
            doc="Number of high pressure stages not including inlet stage",
        ),
    )
    config.declare(
        "num_ip",
        ConfigValue(
            default=10,
            domain=int,
            description="Number of intermediate pressure stages",
            doc="Number of intermediate pressure stages",
        ),
    )
    config.declare(
        "num_lp",
        ConfigValue(
            default=5,
            domain=int,
            description=
            "Number of low pressure stages not including outlet stage",
            doc="Number of low pressure stages not including outlet stage",
        ),
    )
    config.declare(
        "hp_split_locations",
        ConfigList(
            default=[],
            domain=int,
            description="Locations of splitters in HP section",
            doc="A list of index locations of splitters in the HP section. The "
            "indexes indicate after which stage to include splitters.  0 is "
            "between the inlet stage and the first regular HP stage.",
        ),
    )
    config.declare(
        "ip_split_locations",
        ConfigList(
            default=[],
            domain=int,
            description="Locations of splitters in IP section",
            doc="A list of index locations of splitters in the IP section. The "
            "indexes indicate after which stage to include splitters.",
        ),
    )
    config.declare(
        "lp_split_locations",
        ConfigList(
            default=[],
            domain=int,
            description="Locations of splitter in LP section",
            doc="A list of index locations of splitters in the LP section. The "
            "indexes indicate after which stage to include splitters.",
        ),
    )
    config.declare(
        "hp_disconnect",
        ConfigList(
            default=[],
            domain=int,
            description="HP Turbine stages to not connect to next with an arc.",
            doc="HP Turbine stages to not connect to next with an arc. This is "
            "usually used to insert additional units between stages on a "
            "flowsheet, such as a reheater",
        ),
    )
    config.declare(
        "ip_disconnect",
        ConfigList(
            default=[],
            domain=int,
            description="IP Turbine stages to not connect to next with an arc.",
            doc="IP Turbine stages to not connect to next with an arc. This is "
            "usually used to insert additional units between stages on a "
            "flowsheet, such as a reheater",
        ),
    )
    config.declare(
        "lp_disconnect",
        ConfigList(
            default=[],
            domain=int,
            description="LP Turbine stages to not connect to next with an arc.",
            doc="LP Turbine stages to not connect to next with an arc. This is "
            "usually used to insert additional units between stages on a "
            "flowsheet, such as a reheater",
        ),
    )
    config.declare(
        "hp_split_num_outlets",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Dict, hp split index: number of splitter outlets, if not 2",
        ),
    )
    config.declare(
        "ip_split_num_outlets",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Dict, ip split index: number of splitter outlets, if not 2",
        ),
    )
    config.declare(
        "lp_split_num_outlets",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Dict, lp split index: number of splitter outlets, if not 2",
        ),
    )
Exemple #3
0
class PressureChangerData(UnitModelBlockData):
    """
    Standard Compressor/Expander Unit Model Class
    """
    CONFIG = UnitModelBlockData.CONFIG()

    CONFIG.declare("material_balance_type", ConfigValue(
        default=MaterialBalanceType.useDefault,
        domain=In(MaterialBalanceType),
        description="Material balance construction flag",
        doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare("energy_balance_type", ConfigValue(
        default=EnergyBalanceType.useDefault,
        domain=In(EnergyBalanceType),
        description="Energy balance construction flag",
        doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare("momentum_balance_type", ConfigValue(
        default=MomentumBalanceType.pressureTotal,
        domain=In(MomentumBalanceType),
        description="Momentum balance construction flag",
        doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare("has_phase_equilibrium", ConfigValue(
     default=False,
     domain=In([True, False]),
     description="Phase equilibrium construction flag",
     doc="""Indicates whether terms for phase equilibrium should be
constructed, **default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}"""))
    CONFIG.declare("compressor", ConfigValue(
        default=True,
        domain=In([True, False]),
        description="Compressor flag",
        doc="""Indicates whether this unit should be considered a
            compressor (True (default), pressure increase) or an expander
            (False, pressure decrease)."""))
    CONFIG.declare("thermodynamic_assumption", ConfigValue(
        default=ThermodynamicAssumption.isothermal,
        domain=In(ThermodynamicAssumption),
        description="Thermodynamic assumption to use",
        doc="""Flag to set the thermodynamic assumption to use for the unit.
                - ThermodynamicAssumption.isothermal (default)
                - ThermodynamicAssumption.isentropic
                - ThermodynamicAssumption.pump
                - ThermodynamicAssumption.adiabatic"""))
    CONFIG.declare("property_package", ConfigValue(
        default=useDefault,
        domain=is_physical_parameter_block,
        description="Property package to use for control volume",
        doc="""Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}"""))
    CONFIG.declare("property_package_args", ConfigBlock(
        implicit=True,
        description="Arguments to use for constructing property packages",
        doc="""A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))

    def build(self):
        """

        Args:
            None

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        Args:
            None

        Returns:
            None
        """

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

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

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

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

        Args:
            None

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

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

        Args:
            None

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

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

        Args:
            None

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

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

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

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

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

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

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

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

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

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

        Args:
            None

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        opt = SolverFactory(solver)
        opt.options = optarg

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

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

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

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

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

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

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

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

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

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

        return {"vars": var_dict}
Exemple #4
0
def _trf_config():
    """
    Generate the configuration dictionary.
    The user may change the configuration options during the instantiation
    of the trustregion solver:
        >>> optTRF = SolverFactory('trustregion',
        ...                        solver='ipopt',
        ...                        maximum_iterations=50,
        ...                        minimum_radius=1e-5,
        ...                        verbose=True)

    The user may also update the configuration after instantiation:
        >>> optTRF = SolverFactory('trustregion')
        >>> optTRF._CONFIG.trust_radius = 0.5

    The user may also update the configuration as part of the solve call:
        >>> optTRF = SolverFactory('trustregion')
        >>> optTRF.solve(model, decision_variables, trust_radius=0.5)
    Returns
    -------
    CONFIG : ConfigDict
        This holds all configuration options to be passed to the TRF solver.

    """
    CONFIG = ConfigDict('TrustRegion')

    ### Solver options
    CONFIG.declare(
        'solver',
        ConfigValue(default='ipopt',
                    description='Solver to use. Default = ``ipopt``.'))
    CONFIG.declare(
        'keepfiles',
        ConfigValue(default=False,
                    domain=Bool,
                    description="Optional. Whether or not to "
                    "write files of sub-problems for use in debugging. "
                    "Default = False."))
    CONFIG.declare(
        'tee',
        ConfigValue(default=False,
                    domain=Bool,
                    description="Optional. Sets the ``tee`` "
                    "for sub-solver(s) utilized. "
                    "Default = False."))

    ### Trust Region specific options
    CONFIG.declare(
        'verbose',
        ConfigValue(default=False,
                    domain=Bool,
                    description="Optional. When True, print each "
                    "iteration's relevant information to the console "
                    "as well as to the log. "
                    "Default = False."))
    CONFIG.declare(
        'trust_radius',
        ConfigValue(default=1.0,
                    domain=PositiveFloat,
                    description="Initial trust region radius ``delta_0``. "
                    "Default = 1.0."))
    CONFIG.declare(
        'minimum_radius',
        ConfigValue(
            default=1e-6,
            domain=PositiveFloat,
            description="Minimum allowed trust region radius ``delta_min``. "
            "Default = 1e-6."))
    CONFIG.declare(
        'maximum_radius',
        ConfigValue(
            default=CONFIG.trust_radius * 100,
            domain=PositiveFloat,
            description="Maximum allowed trust region radius. If trust region "
            "radius reaches maximum allowed, solver will exit. "
            "Default = 100 * trust_radius."))
    CONFIG.declare(
        'maximum_iterations',
        ConfigValue(default=50,
                    domain=PositiveInt,
                    description="Maximum allowed number of iterations. "
                    "Default = 50."))
    ### Termination options
    CONFIG.declare(
        'feasibility_termination',
        ConfigValue(
            default=1e-5,
            domain=PositiveFloat,
            description=
            "Feasibility measure termination tolerance ``epsilon_theta``. "
            "Default = 1e-5."))
    CONFIG.declare(
        'step_size_termination',
        ConfigValue(
            default=CONFIG.feasibility_termination,
            domain=PositiveFloat,
            description="Step size termination tolerance ``epsilon_s``. "
            "Matches the feasibility termination tolerance by default."))
    ### Switching Condition options
    CONFIG.declare(
        'minimum_feasibility',
        ConfigValue(default=1e-4,
                    domain=PositiveFloat,
                    description="Minimum feasibility measure ``theta_min``. "
                    "Default = 1e-4."))
    CONFIG.declare(
        'switch_condition_kappa_theta',
        ConfigValue(
            default=0.1,
            domain=In(NumericRange(0, 1, 0, (False, False))),
            description="Switching condition parameter ``kappa_theta``. "
            "Contained in open set (0, 1). "
            "Default = 0.1."))
    CONFIG.declare(
        'switch_condition_gamma_s',
        ConfigValue(default=2.0,
                    domain=PositiveFloat,
                    description="Switching condition parameter ``gamma_s``. "
                    "Must satisfy: ``gamma_s > 1/(1+mu)`` where ``mu`` "
                    "is contained in set (0, 1]. "
                    "Default = 2.0."))
    ### Trust region update/ratio test parameters
    CONFIG.declare(
        'radius_update_param_gamma_c',
        ConfigValue(
            default=0.5,
            domain=In(NumericRange(0, 1, 0, (False, False))),
            description="Lower trust region update parameter ``gamma_c``. "
            "Default = 0.5."))
    CONFIG.declare(
        'radius_update_param_gamma_e',
        ConfigValue(
            default=2.5,
            domain=In(NumericRange(1, None, 0)),
            description="Upper trust region update parameter ``gamma_e``. "
            "Default = 2.5."))
    CONFIG.declare(
        'ratio_test_param_eta_1',
        ConfigValue(default=0.05,
                    domain=In(NumericRange(0, 1, 0, (False, False))),
                    description="Lower ratio test parameter ``eta_1``. "
                    "Must satisfy: ``0 < eta_1 <= eta_2 < 1``. "
                    "Default = 0.05."))
    CONFIG.declare(
        'ratio_test_param_eta_2',
        ConfigValue(default=0.2,
                    domain=In(NumericRange(0, 1, 0, (False, False))),
                    description="Lower ratio test parameter ``eta_2``. "
                    "Must satisfy: ``0 < eta_1 <= eta_2 < 1``. "
                    "Default = 0.2."))
    ### Filter
    CONFIG.declare(
        'maximum_feasibility',
        ConfigValue(
            default=50.0,
            domain=PositiveFloat,
            description="Maximum allowable feasibility measure ``theta_max``. "
            "Parameter for use in filter method."
            "Default = 50.0."))
    CONFIG.declare(
        'param_filter_gamma_theta',
        ConfigValue(
            default=0.01,
            domain=In(NumericRange(0, 1, 0, (False, False))),
            description="Fixed filter parameter ``gamma_theta`` within (0, 1). "
            "Default = 0.01"))
    CONFIG.declare(
        'param_filter_gamma_f',
        ConfigValue(
            default=0.01,
            domain=In(NumericRange(0, 1, 0, (False, False))),
            description="Fixed filter parameter ``gamma_f`` within (0, 1). "
            "Default = 0.01"))

    return CONFIG
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=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 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=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_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.}"""))
    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=list_of_floats,
        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().config.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
        # TODO : Need to add units
        self.volume = Var(initialize=1,
                          doc="Reactor 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 != '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 = {"Volume": self.volume}
        var_dict = {"Length": self.length}
        var_dict = {"Area": self.area}

        return {"vars": var_dict}
Exemple #6
0
class PyomoCyIpoptSolver(object):

    CONFIG = ConfigBlock("cyipopt")
    CONFIG.declare("tee", ConfigValue(
        default=False,
        domain=bool,
        description="Stream solver output to console",
    ))
    CONFIG.declare("load_solutions", ConfigValue(
        default=True,
        domain=bool,
        description="Store the final solution into the original Pyomo model",
    ))
    CONFIG.declare("return_nlp", ConfigValue(
        default=False,
        domain=bool,
        description="Return the results object and the underlying nlp"
                    " NLP object from the solve call.",
    ))
    CONFIG.declare("options", ConfigBlock(implicit=True))
    CONFIG.declare("intermediate_callback", ConfigValue(
        default=None,
        description="Set the function that will be called each"
                    " iteration."
    ))

    def __init__(self, **kwds):
        """Create an instance of the CyIpoptSolver. You must
        provide a problem_interface that corresponds to
        the abstract class CyIpoptProblemInterface

        options can be provided as a dictionary of key value
        pairs
        """
        self.config = self.CONFIG(kwds)

    def _set_model(self, model):
        self._model = model

    def available(self, exception_flag=False):
        return numpy_available and cyipopt_available

    def license_is_valid(self):
        return True

    def version(self):
        return tuple(int(_) for _ in cyipopt.__version__.split('.'))

    def solve(self, model, **kwds):
        config = self.config(kwds, preserve_implicit=True)

        if not isinstance(model, Block):
            raise ValueError("PyomoCyIpoptSolver.solve(model): model "
                             "must be a Pyomo Block")

        # If this is a Pyomo model / block, then we need to create
        # the appropriate PyomoNLP, then wrap it in a CyIpoptNLP
        grey_box_blocks = list(model.component_data_objects(
            egb.ExternalGreyBoxBlock, active=True))
        if grey_box_blocks:
            # nlp = pyomo_nlp.PyomoGreyBoxNLP(model)
            nlp = pyomo_grey_box.PyomoNLPWithGreyBoxBlocks(model)
        else:
            nlp = pyomo_nlp.PyomoNLP(model)

        problem = CyIpoptNLP(nlp, intermediate_callback=config.intermediate_callback)

        xl = problem.x_lb()
        xu = problem.x_ub()
        gl = problem.g_lb()
        gu = problem.g_ub()

        nx = len(xl)
        ng = len(gl)

        cyipopt_solver = cyipopt.Problem(
            n=nx,
            m=ng,
            problem_obj=problem,
            lb=xl,
            ub=xu,
            cl=gl,
            cu=gu
        )

        # check if we need scaling
        obj_scaling, x_scaling, g_scaling = problem.scaling_factors()
        if any(_ is not None for _ in (obj_scaling, x_scaling, g_scaling)):
            # need to set scaling factors
            if obj_scaling is None:
                obj_scaling = 1.0
            if x_scaling is None:
                x_scaling = np.ones(nx)
            if g_scaling is None:
                g_scaling = np.ones(ng)
            try:
                set_scaling = cyipopt_solver.set_problem_scaling
            except AttributeError:
                # Fall back to pre-1.0.0 API
                set_scaling = cyipopt_solver.setProblemScaling
            set_scaling(obj_scaling, x_scaling, g_scaling)

        # add options
        try:
            add_option = cyipopt_solver.add_option
        except AttributeError:
            # Fall back to pre-1.0.0 API
            add_option = cyipopt_solver.addOption
        for k, v in config.options.items():
            add_option(k, v)

        timer = TicTocTimer()
        try:
            # We preemptively set up the TeeStream, even if we aren't
            # going to use it: the implementation is such that the
            # context manager does nothing (i.e., doesn't start up any
            # processing threads) until afer a client accesses
            # STDOUT/STDERR
            with TeeStream(sys.stdout) as _teeStream:
                if config.tee:
                    try:
                        fd = sys.stdout.fileno()
                    except (io.UnsupportedOperation, AttributeError):
                        # If sys,stdout doesn't have a valid fileno,
                        # then create one using the TeeStream
                        fd = _teeStream.STDOUT.fileno()
                else:
                    fd = None
                with redirect_fd(fd=1, output=fd, synchronize=False):
                    x, info = cyipopt_solver.solve(problem.x_init())
            solverStatus = SolverStatus.ok
        except:
            msg = "Exception encountered during cyipopt solve:"
            logger.error(msg, exc_info=sys.exc_info())
            solverStatus = SolverStatus.unknown
            raise

        wall_time = timer.toc(None)

        results = SolverResults()

        if config.load_solutions:
            nlp.set_primals(x)
            nlp.set_duals(info['mult_g'])
            nlp.load_state_into_pyomo(
                bound_multipliers=(info['mult_x_L'], info['mult_x_U']))
        else:
            soln = results.solution.add()
            soln.variable.update(
                (i, {'Value':j, 'ipopt_zL_out': zl, 'ipopt_zU_out': zu})
                for i,j,zl,zu in zip( nlp.variable_names(),
                                      x,
                                      info['mult_x_L'],
                                      info['mult_x_U'] )
            )
            soln.constraint.update(
                (i, {'Dual':j}) for i,j in zip(
                    nlp.constraint_names(), info['mult_g']))


        results.problem.name = model.name
        obj = next(model.component_data_objects(Objective, active=True))
        if obj.sense == minimize:
            results.problem.sense = ProblemSense.minimize
            results.problem.upper_bound = info['obj_val']
        else:
            results.problem.sense = ProblemSense.maximize
            results.problem.lower_bound = info['obj_val']
        results.problem.number_of_objectives = 1
        results.problem.number_of_constraints = ng
        results.problem.number_of_variables = nx
        results.problem.number_of_binary_variables = 0
        results.problem.number_of_integer_variables = 0
        results.problem.number_of_continuous_variables = nx
        # TODO: results.problem.number_of_nonzeros

        results.solver.name = 'cyipopt'
        results.solver.return_code = info['status']
        results.solver.message = info['status_msg']
        results.solver.wallclock_time = wall_time
        status_enum = _cyipopt_status_enum[info['status_msg']]
        results.solver.termination_condition = _ipopt_term_cond[status_enum]
        results.solver.status = TerminationCondition.to_solver_status(
            results.solver.termination_condition)

        if config.return_nlp:
            return results, nlp

        return results

    #
    # Support "with" statements.
    #
    def __enter__(self):
        return self

    def __exit__(self, t, v, traceback):
        pass
class PackedColumnData(UnitModelBlockData):
    """
    Standard Continous Differential Contactor (CDC) Model Class.

    """

    # Configuration template for unit level arguments applicable to both phases
    CONFIG = UnitModelBlockData.CONFIG()

    # Configuration template for phase specific  arguments
    _PhaseCONFIG = ConfigBlock()

    CONFIG.declare("finite_elements", ConfigValue(
        default=20,
        domain=int,
        description="Number of finite elements length domain",
        doc="""Number of finite elements to use when discretizing length
domain (default=20)"""))

    CONFIG.declare("length_domain_set", ConfigValue(
        default=[0.0, 1.0],
        domain=list,
        description="List of points in length domain",
        doc="""length_domain_set - (optional) list of point to use to
initialize a new ContinuousSet if length_domain is not
provided (default = [0.0, 1.0])"""))

    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".
**Valid values:** {
**"dae.finite_difference"** - Use a finite difference transformation method,
**"dae.collocation"** - use a collocation transformation method}"""))

    CONFIG.declare("collocation_points", ConfigValue(
        default=3,
        domain=int,
        description="Number of collocation points per finite element",
        doc="""Number of collocation points to use per finite element when
discretizing length domain (default=3)"""))

    CONFIG.declare("column_pressure_drop", ConfigValue(
        default=0,
        description="Column pressure drop per unit length in Pa/m",
        doc="Column pressure drop per unit length in Pa/m provided as a value or expression"))

    # Populate the phase side template to default values
    _PhaseCONFIG.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.}"""))

    _PhaseCONFIG.declare("property_package", ConfigValue(
        default=None,
        domain=is_physical_parameter_block,
        description="Property package to use for control volume",
        doc="""Property parameter object used to define property calculations
(default = 'use_parent_value')
- 'use_parent_value' - get package from parent (default = None)
- a ParameterBlock object"""))

    _PhaseCONFIG.declare("property_package_args", ConfigValue(
        default={},
        description="Arguments for constructing vapor property package",
        doc="""A dict of arguments to be passed to the PropertyBlockData
and used when constructing these
(default = 'use_parent_value')
- 'use_parent_value' - get package from parent (default = None)
- a dict (see property package for documentation)
            """))
            
    _PhaseCONFIG.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".
**Valid values:** {
**"BACKWARD"** - Use a BACKWARD finite difference transformation method,
**"FORWARD""** - Use a FORWARD finite difference transformation method,
**"LAGRANGE-RADAU""** - use a collocation transformation method}"""))

    # Create individual config blocks for vapor(gas) and liquid sides
    CONFIG.declare("vapor_side",
                   _PhaseCONFIG(doc="vapor side config arguments"))

    CONFIG.declare("liquid_side",
                   _PhaseCONFIG(doc="liquid side config arguments"))

    # =========================================================================

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

        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to build default attributes
        super().build()

    # =========================================================================
        """ Set argument values for vapor and liquid sides"""

        # Set flow directions for the control volume blocks
        # Gas flows from 0 to 1, Liquid flows from 1 to 0
        
        # TODO: Only handling countercurrent flow for now.
        set_direction_vapor = FlowDirection.forward
        set_direction_liquid = FlowDirection.backward

    # =========================================================================
        """ Build Control volume 1D for vapor phase and
            populate vapor control volume"""

        self.vapor_phase = ControlVolume1DBlock(default={
            "transformation_method": self.config.transformation_method,
            "transformation_scheme":
                self.config.vapor_side.transformation_scheme,
            "finite_elements": self.config.finite_elements,
            "collocation_points": self.config.collocation_points,
            "dynamic": self.config.dynamic,
            "has_holdup": self.config.has_holdup,
            "area_definition": DistributedVars.variant,
            "property_package": self.config.vapor_side.property_package,
            "property_package_args":
                self.config.vapor_side.property_package_args})

        self.vapor_phase.add_geometry(
            flow_direction=set_direction_vapor,
            length_domain_set=self.config.length_domain_set)

        self.vapor_phase.add_state_blocks(
            information_flow=set_direction_vapor,
            has_phase_equilibrium=False)

        self.vapor_phase.add_material_balances(
            balance_type=MaterialBalanceType.componentTotal,
            has_phase_equilibrium=False,
            has_mass_transfer=True)

        self.vapor_phase.add_energy_balances(
            balance_type=EnergyBalanceType.enthalpyTotal,
            has_heat_transfer=True)

        self.vapor_phase.add_momentum_balances(
            balance_type=MomentumBalanceType.pressureTotal,
            has_pressure_change=self.config.vapor_side.has_pressure_change)

        self.vapor_phase.apply_transformation()

    # ==========================================================================
        """ Build Control volume 1D for liquid phase and
            populate liquid control volume

        """
        self.liquid_phase = ControlVolume1DBlock(default={
            "transformation_method": self.config.transformation_method,
            "transformation_scheme":
                self.config.liquid_side.transformation_scheme,
            "finite_elements": self.config.finite_elements,
            "collocation_points": self.config.collocation_points,
            "dynamic": self.config.dynamic,
            "has_holdup": self.config.has_holdup,
            "area_definition": DistributedVars.variant,
            "property_package": self.config.liquid_side.property_package,
            "property_package_args":
                self.config.liquid_side.property_package_args})

        self.liquid_phase.add_geometry(flow_direction=set_direction_liquid,
                                       length_domain_set=self.config.
                                       length_domain_set)

        self.liquid_phase.add_state_blocks(
            information_flow=set_direction_liquid,
            has_phase_equilibrium=False)

        self.liquid_phase.add_material_balances(
            balance_type=MaterialBalanceType.componentTotal,
            has_phase_equilibrium=False,
            has_mass_transfer=True)

        self.liquid_phase.add_energy_balances(
            balance_type=EnergyBalanceType.enthalpyTotal,
            has_heat_transfer=True)

        self.liquid_phase.apply_transformation()

        # Add Ports for vapor side
        self.add_inlet_port(name="vapor_inlet", block=self.vapor_phase)
        self.add_outlet_port(name="vapor_outlet", block=self.vapor_phase)

        # Add Ports for liquid side
        self.add_inlet_port(name="liquid_inlet", block=self.liquid_phase)
        self.add_outlet_port(name="liquid_outlet", block=self.liquid_phase)

    # ==========================================================================
        """ Add performace equation method"""
        self._make_performance()

    def _make_performance(self):
        """
        Constraints for unit model.

        Args: None

        Returns: None

        """

        # ======================================================================
        # Custom Sets
        vap_comp = self.config.vapor_side.property_package.component_list
        liq_comp = self.config.liquid_side.property_package.component_list
        equilibrium_comp = vap_comp & liq_comp
        solvent_comp_list = \
            self.config.liquid_side.property_package.solvent_set
        solute_comp_list = self.config.liquid_side.property_package.solute_set
        vapor_phase_list_ref = \
            self.config.vapor_side.property_package.phase_list
        liquid_phase_list_ref = \
            self.config.liquid_side.property_package.phase_list

        # Packing  parameters
        self.eps_ref = Param(initialize=0.97,units=None,
                             mutable=True,
                             doc="Packing void space m3/m3")

        self.packing_specific_area = Param(initialize=250,units=pyunits.m**2 / pyunits.m**3,
                           mutable=True,
                           doc="Packing specific surface area (m2/m3)")
        
        self.packing_channel_size = Param(initialize=0.1,units=pyunits.m,
                           mutable=True,
                           doc="Packing channel size (m)")
        
        self.hydraulic_diameter = Expression(expr=4 * self.eps_ref / self.packing_specific_area,
                                 doc="Hydraulic diameter (m)")

        # Add the integer indices along vapor phase length domain
        self.zi = Param(self.vapor_phase.length_domain, mutable=True,
                        doc='''Integer indexing parameter required for transfer
                             across boundaries of a given volume element''')
                             
        # Set the integer indices along vapor phase length domain
        for i, x in enumerate(self.vapor_phase.length_domain, 1):
            self.zi[x] = i

        # Unit Model Design Variables
        # Geometry
        self.diameter_column = Var(domain=Reals,
                                   initialize=0.1,
                                   units=pyunits.m,
                                   doc='Column diameter')
        
        self.area_column = Var(domain=Reals,
                               initialize=0.5,
                               units=pyunits.m**2,
                               doc='Column cross-sectional area')
        
        self.length_column = Var(domain=Reals,
                                 initialize=4.9,
                                 units=pyunits.m,
                                 doc='Column length')

        # Hydrodynamics
        self.velocity_vap = Var(self.flowsheet().time,
                                self.vapor_phase.length_domain,
                                domain=NonNegativeReals,
                                initialize=2,
                                units=pyunits.m / pyunits.s,
                                doc='Vapor superficial velocity')
        
        self.velocity_liq = Var(self.flowsheet().time,
                                self.liquid_phase.length_domain,
                                domain=NonNegativeReals,
                                initialize=0.01,
                                units=pyunits.m / pyunits.s,
                                doc='Liquid superficial velocity')
        
        self.holdup_liq = Var(self.flowsheet().time,
                              self.liquid_phase.length_domain,
                              initialize=0.001,
                              doc='Volumetric liquid holdup [-]')

        def rule_holdup_vap(blk, t, x):
            return blk.eps_ref - blk.holdup_liq[t, x]

        self.holdup_vap = Expression(self.flowsheet().time,
                                     self.vapor_phase.length_domain,
                                     rule=rule_holdup_vap,
                                     doc='Volumetric vapor holdup [-]')
        
        # Define gas velocity at flooding point (m/s)                
        self.gas_velocity_flood = Var(self.flowsheet().time,
                                self.vapor_phase.length_domain,
                                initialize=1,
                                doc='Gas velocity at flooding point')
            
        # Flooding fraction 
        def rule_flood_fraction(blk, t, x):
            return blk.velocity_vap[t, x]/blk.gas_velocity_flood[t, x]
            
        self.flood_fraction = Expression(self.flowsheet().time,
                                    self.vapor_phase.length_domain,
                                    rule=rule_flood_fraction,
                                    doc='Flooding fraction (expected to be below 0.8)')
        
        # Mass and heat transfer terms
        
        # Mass transfer terms
        self.pressure_equil = Var(
            self.flowsheet().time,
            self.vapor_phase.length_domain,
            equilibrium_comp,
            domain=NonNegativeReals,
            initialize=500,
            units=pyunits.Pa,
            doc='Equilibruim pressure of diffusing components at interface')
        
        self.interphase_mass_transfer = Var(
            self.flowsheet().time,
            self.liquid_phase.length_domain,
            equilibrium_comp,
            domain=Reals,
            initialize=0.1,
            units=pyunits.mol / (pyunits.s * pyunits.m),
            doc='Rate at which moles of diffusing species transfered into liquid')
        
        self.enhancement_factor = Var(self.flowsheet().time,
                                      self.liquid_phase.length_domain,
                                      units=None,
                                      initialize=160,
                                      doc='Enhancement factor')

        # Heat transfer terms
        self.heat_flux_vap = Var(self.flowsheet().time,
                            self.vapor_phase.length_domain,
                            domain=Reals,
                            initialize=0.0,
                            units=pyunits.J / (pyunits.s * (pyunits.m**3)),
                            doc='Volumetric heat flux in vapor phase')

        # =====================================================================
        # Add performance equations

        # Inter-facial Area model ([m2/m3]):

        self.area_interfacial = Var(self.flowsheet().time,
                                    self.vapor_phase.length_domain,
                                    initialize=0.9,
                                    doc='Specific inter-facial area')

        # ---------------------------------------------------------------------
        # Geometry constraints

        # Column area [m2]
        @self.Constraint(doc="Column cross-sectional area")
        def column_cross_section_area(blk):
            return blk.area_column == (
                CONST.pi * 0.25 * (blk.diameter_column)**2)

        # Area of control volume : vapor side and liquid side
        control_volume_area_definition = ''' column_area * phase_holdup.
        The void fraction of the vapor phase (volumetric vapor holdup) and that
        of the liquid phase(volumetric liquid holdup) are
        lumped into the definition of the cross-sectional area of the
        vapor-side and liquid-side control volume respectively. Hence, the
        cross-sectional area of the control volume changes with time and space.
        '''

        if self.config.dynamic:
            @self.Constraint(self.flowsheet().time,
                             self.vapor_phase.length_domain,
                             doc=control_volume_area_definition)
            def vapor_side_area(bk, t, x):
                return bk.vapor_phase.area[t, x] == (
                    bk.area_column * bk.holdup_vap[t, x])

            @self.Constraint(self.flowsheet().time,
                             self.liquid_phase.length_domain,
                             doc=control_volume_area_definition)
            def liquid_side_area(bk, t, x):
                return bk.liquid_phase.area[t, x] == (
                    bk.area_column * bk.holdup_liq[t, x])
        else:
            self.vapor_phase.area.fix(value(self.area_column))
            self.liquid_phase.area.fix(value(self.area_column))

        # Pressure consistency in phases
        @self.Constraint(self.flowsheet().time,
                         self.liquid_phase.length_domain,
                         doc='''Mechanical equilibruim: vapor-side pressure
                                    equal liquid -side pressure''')
        def mechanical_equil(bk, t, x):
            return bk.liquid_phase.properties[t, x].pressure == \
                    bk.vapor_phase.properties[t, x].pressure

        # Length of control volume : vapor side and liquid side
        @self.Constraint(doc="Vapor side length")
        def vapor_side_length(blk):
            return blk.vapor_phase.length == blk.length_column

        @self.Constraint(doc="Liquid side length")
        def liquid_side_length(blk):
            return blk.liquid_phase.length == blk.length_column

        # ---------------------------------------------------------------------
        # Hydrodynamic constraints
        # Vapor superficial velocity

        @self.Constraint(self.flowsheet().time,
                         self.vapor_phase.length_domain,
                         doc="Vapor superficial velocity")
        def eq_velocity_vap(blk, t, x):
            return blk.velocity_vap[t, x] * blk.area_column * \
                blk.vapor_phase.properties[t, x].dens_mol == \
                blk.vapor_phase.properties[t, x].flow_mol

        # Liquid superficial velocity
        @self.Constraint(self.flowsheet().time,
                         self.liquid_phase.length_domain,
                         doc="Liquid superficial velocity")
        def eq_velocity_liq(blk, t, x):
            return blk.velocity_liq[t, x] * blk.area_column * \
                blk.liquid_phase.properties[t, x].dens_mol == \
                blk.liquid_phase.properties[t, x].flow_mol

        # ---------------------------------------------------------------------
        # Mass transfer coefficients
        # Mass transfer coefficients of diffusing components in vapor phase [mol/m2.s.Pa]
        self.k_v = Var(self.flowsheet().time,
                       self.vapor_phase.length_domain,
                       equilibrium_comp,
                       doc=' Vapor phase mass transfer coefficient')

        # Mass transfer coefficients of diffusing components in liquid phase  [m/s]
        self.k_l = Var(self.flowsheet().time,
                       self.liquid_phase.length_domain,
                       equilibrium_comp,
                       doc='Liquid phase mass transfer coefficient')

        # Intermediate term
        def rule_phi(blk, t, x, j):
            if x == self.vapor_phase.length_domain.first():
                return Expression.Skip
            else:
                zb = self.vapor_phase.length_domain.at(self.zi[x].value - 1)
                return (blk.enhancement_factor[t, zb] *
                        blk.k_l[t, zb, j] /
                        blk.k_v[t, x, j])

        self.phi = Expression(
            self.flowsheet().time,
            self.vapor_phase.length_domain,
            solute_comp_list,
            rule=rule_phi,
            doc='Equilibruim partial pressure intermediate term for solute')

        # Equilibruim partial pressure of diffusing components at interface
        @self.Constraint(self.flowsheet().time,
                          self.vapor_phase.length_domain,
                          equilibrium_comp,
                          doc='''Equilibruim partial pressure of diffusing
                                components at interface''')
        def pressure_at_interface(blk, t, x, j):
            if x == self.vapor_phase.length_domain.first():
                return blk.pressure_equil[t, x, j] == 0.0
            else:
                zb = self.vapor_phase.length_domain.at(self.zi[x].value - 1)
                lprops = blk.liquid_phase.properties[t, zb]
                henrycomp = lprops.params.get_component(j).config.henry_component
                if henrycomp is not None and "Liq" in henrycomp:
                    return blk.pressure_equil[t, x, j] == (
                        (blk.vapor_phase.properties[t, x].mole_frac_comp[j] *
                          blk.vapor_phase.properties[
                              t, x].pressure + blk.phi[t, x, j] *
                          lprops.conc_mol_phase_comp_true['Liq',j]) /
                        (1 + blk.phi[t, x, j] /
                          blk.liquid_phase.properties[t, zb].henry['Liq',j]))
                else:
                    return blk.pressure_equil[t, x, j] == (
                        lprops.vol_mol_phase['Liq'] *
                        lprops.conc_mol_phase_comp_true['Liq',j] *
                        lprops.pressure_sat_comp[j])

        # Mass transfer of  diffusing components in vapor phase
        def rule_mass_transfer(blk, t, x, j):
            if x == self.vapor_phase.length_domain.first():
                return blk.interphase_mass_transfer[t, x, j] == 0.0
            else:
                return blk.interphase_mass_transfer[t, x, j] == (
                blk.k_v[t, x, j] *
                blk.area_interfacial[t, x] * blk.area_column *
                (blk.vapor_phase.properties[t, x].mole_frac_comp[j] *
                 blk.vapor_phase.properties[t, x].pressure -
                 blk.pressure_equil[t, x, j]))

        self.mass_transfer_vapor = Constraint(self.flowsheet().time,
                                        self.vapor_phase.length_domain,
                                        equilibrium_comp,
                                        rule=rule_mass_transfer,
                                        doc="mass transfer in vapor phase")

        # Liquid phase mass transfer handle
        @self.Constraint(self.flowsheet().time,
                          self.liquid_phase.length_domain,
                          self.liquid_phase.properties.phase_component_set,
                          doc="mass transfer to liquid")
        def liquid_phase_mass_transfer_handle(blk, t, x, p, j):
            if x == self.liquid_phase.length_domain.last():
                return blk.liquid_phase.mass_transfer_term[t, x, p, j] == 0.0
            else:
                zf = self.liquid_phase.length_domain.at(self.zi[x].value + 1)
                if j in equilibrium_comp:
                    return blk.liquid_phase.mass_transfer_term[t, x, p, j] == \
                        blk.interphase_mass_transfer[t, zf, j]
                else:
                    return blk.liquid_phase.mass_transfer_term[t, x, p, j] == \
                        0.0

        # Vapor phase mass transfer handle
        @self.Constraint(self.flowsheet().time,
                         self.vapor_phase.length_domain,
                         self.vapor_phase.properties.phase_component_set,
                         doc="mass transfer from vapor")
        def vapor_phase_mass_transfer_handle(blk, t, x, p, j):
            if x == self.vapor_phase.length_domain.first():
                return blk.vapor_phase.mass_transfer_term[t, x, p, j] == 0.0
            else:
                if j in equilibrium_comp:
                    return blk.vapor_phase.mass_transfer_term[t, x, p, j] == \
                        -blk.interphase_mass_transfer[t, x, j]
                else:
                    return blk.vapor_phase.mass_transfer_term[t, x, p, j] == \
                        0.0

        # Heat transfer coefficients
        # Vapor-liquid heat transfer coefficient [J/m2.s.K]

        self.h_v = Var(self.flowsheet().time,
                       self.vapor_phase.length_domain,
                       initialize=100,
                       doc='''Vapor-liquid heat transfer coefficient''')

        # Vapor-liquid heat transfer coeff modified by Ackmann factor [J/m.s.K]
        def rule_heat_transfer_coeff_Ack(blk, t, x):
            if x == self.vapor_phase.length_domain.first():
                return Expression.Skip
            else:
                Ackmann_factor =\
                    sum(blk.vapor_phase.properties[t, x].cp_mol_phase_comp['Vap',j] *
                     blk.interphase_mass_transfer[t, x, j] for j in equilibrium_comp)
                return Ackmann_factor /\
                    (1 - exp(-Ackmann_factor /
                             (blk.h_v[t, x] * blk.area_interfacial[t, x] *
                              blk.area_column)))
        self.h_v_Ack = Expression(
            self.flowsheet().time,
            self.vapor_phase.length_domain,
            rule=rule_heat_transfer_coeff_Ack,
            doc='Vap-Liq heat transfer coefficient corrected by Ackmann factor')

        # Heat flux  - vapor side [J/s.m]
        @self.Constraint(self.flowsheet().time,
                         self.vapor_phase.length_domain,
                         doc="heat transfer - vapor side ")
        def vapor_phase_volumetric_heat_flux(blk, t, x):
            if x == self.vapor_phase.length_domain.first():
                return blk.heat_flux_vap[t, x] == 0
            else:
                zb = self.vapor_phase.length_domain.at(value(self.zi[x]) - 1)
                return blk.heat_flux_vap[t, x] == blk.h_v_Ack[t, x] * \
                    (blk.liquid_phase.properties[t, zb].temperature -
                      blk.vapor_phase.properties[t, x].temperature)
                    
        # Heat transfer - vapor side [J/s.m]
        @self.Constraint(self.flowsheet().time,
                         self.vapor_phase.length_domain,
                         doc="heat transfer - vapor side ")
        def vapor_phase_heat_transfer(blk, t, x):
            if x == self.vapor_phase.length_domain.first():
                return blk.vapor_phase.heat[t, x] == 0
            else:
                zb = self.vapor_phase.length_domain.at(value(self.zi[x]) - 1)
                return blk.vapor_phase.heat[t, x] == -blk.heat_flux_vap[t, x] - \
                    (sum(blk.vapor_phase.properties[t, x].enth_mol_phase_comp['Vap',j] *
                      blk.vapor_phase.mass_transfer_term[t, x, 'Vap', j] for j in solute_comp_list)) + \
                    (sum(blk.liquid_phase.properties[t, zb].enth_mol_phase_comp['Liq',j] *
                      blk.liquid_phase.mass_transfer_term[t, zb, 'Liq', j] for j in solvent_comp_list))

        # Heat transfer - liquid side [J/s.m]
        @self.Constraint(self.flowsheet().time,
                          self.liquid_phase.length_domain,
                          doc="heat transfer - liquid side ")
        def liquid_phase_heat_transfer(blk, t, x):
            if x == self.liquid_phase.length_domain.last():
                return blk.liquid_phase.heat[t, x] == 0
            else:
                zf = self.vapor_phase.length_domain.at(value(self.zi[x]) + 1)
                return blk.liquid_phase.heat[t, x] == -blk.vapor_phase.heat[t, zf]

    # =========================================================================
    # Model initialization routine

    def initialize(blk,
                   vapor_phase_state_args=None,
                   liquid_phase_state_args=None,
                   state_vars_fixed=False,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        """
        Column initialization.

        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 = None).
            optarg : solver options dictionary object (default=None, use
                     default solver options)
            solver : str indicating which solver to use during initialization
                    (default = None, use IDAES default solver)

        """

        # Set up logger for initialization and solve
        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)

        dynamic_constraints = [
            "pressure_at_interface",
            "mass_transfer_vapor",
            "liquid_phase_mass_transfer_handle",
            "vapor_phase_mass_transfer_handle",
            "vapor_phase_volumetric_heat_flux",
            "vapor_phase_heat_transfer",
            "liquid_phase_heat_transfer"]

        # ---------------------------------------------------------------------
        # Deactivate unit model level constraints (asides geometry constraints)
        for c in blk.component_objects(Constraint, descend_into=True):
            if c.local_name in dynamic_constraints:
                c.deactivate()

        # Fix variables

        # Interface pressure
        blk.pressure_equil.fix()

        # Molar flux
        blk.interphase_mass_transfer.fix(0.0)
        blk.vapor_phase.mass_transfer_term.fix(0.0)
        blk.liquid_phase.mass_transfer_term.fix(0.0)

        # Heat transfer rate
        blk.heat_flux_vap.fix(0.0)
        blk.vapor_phase.heat.fix(0.0)
        blk.liquid_phase.heat.fix(0.0)
        
        # # ---------------------------------------------------------------------
        # Provide state arguments for property package initialization

        init_log.info("Step 1: Property Package initialization")
        
        vap_comp = blk.config.vapor_side.property_package.component_list
        liq_apparent_comp = [c[1] for c in blk.liquid_phase.properties.phase_component_set]
        
        if vapor_phase_state_args is None:
            vapor_phase_state_args = {
                'flow_mol': blk.vapor_inlet.flow_mol[0].value,
                'temperature': blk.vapor_inlet.temperature[0].value,
                'pressure': blk.vapor_inlet.pressure[0].value,
                'mole_frac_comp':
                {j: blk.vapor_inlet.mole_frac_comp[0, j].value 
                 for j in vap_comp}}

        if liquid_phase_state_args is None:
            liquid_phase_state_args = {
                'flow_mol': blk.liquid_inlet.flow_mol[0].value,
                'temperature': blk.liquid_inlet.temperature[0].value,
                'pressure': blk.vapor_inlet.pressure[0].value,
                'mole_frac_comp':
                {j: blk.liquid_inlet.mole_frac_comp[0, j].value 
                 for j in liq_apparent_comp}}

        # Initialize vapor_phase properties block
        vflag = blk.vapor_phase.properties.initialize(
            state_args=vapor_phase_state_args,
            state_vars_fixed=False,
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=True)

        # Initialize liquid_phase properties block
        lflag = blk.liquid_phase.properties.initialize(
            state_args=liquid_phase_state_args,
            state_vars_fixed=False,
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=True)

        init_log.info("Step 2: Steady-State isothermal mass balance")
        
        blk.vapor_phase.properties.release_state(flags=vflag)
        
        blk.liquid_phase.properties.release_state(flags=lflag)
        
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Step 2: {}.".format(idaeslog.condition(res)))
        
        assert res.solver.termination_condition == \
            TerminationCondition.optimal
        assert res.solver.status == SolverStatus.ok
        
        # ---------------------------------------------------------------------
        init_log.info('Step 3: Interface equilibrium')
        
        # Activate interface pressure constraint 
        
        blk.pressure_equil.unfix()
        blk.pressure_at_interface.activate()
        
        # ----------------------------------------------------------------------

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

        init_log.info('Step 4: Isothermal chemical absoption')
        init_log.info_high("No mass transfer to mass transfer")

        # Unfix mass transfer terms
        blk.interphase_mass_transfer.unfix()

        # Activate mass transfer equation in vapor phase
        blk.mass_transfer_vapor.activate()
        
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        
        blk.vapor_phase.mass_transfer_term.unfix()
        blk.liquid_phase.mass_transfer_term.unfix()
        blk.vapor_phase_mass_transfer_handle.activate()
        blk.liquid_phase_mass_transfer_handle.activate()
        
        optarg = {
            "tol": 1e-8,
            "max_iter": 150,
            "bound_push":1e-8}
        opt.options = optarg
                    
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
            if res.solver.status != SolverStatus.warning:
                print('')
        init_log.info_high(
            "Step 4 complete: {}.".format(idaeslog.condition(res)))

        # ---------------------------------------------------------------------
        init_log.info('Step 5: Adiabatic chemical absoption')
        init_log.info_high("Isothermal to Adiabatic ")
        
        # Unfix heat transfer terms
        blk.heat_flux_vap.unfix()
        blk.vapor_phase.heat.unfix()
        blk.liquid_phase.heat.unfix()

        # Activate heat transfer and steady-state energy balance related equations
        for c in ["vapor_phase_volumetric_heat_flux",
                  "vapor_phase_heat_transfer",
                  "liquid_phase_heat_transfer"]:
            getattr(blk, c).activate()
            
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:    
            res = opt.solve(blk, tee=slc.tee)

        init_log.info_high(
            "Step 5 complete: {}.".format(idaeslog.condition(res)))

        # ---------------------------------------------------------------------

        if not blk.config.dynamic:
            init_log.info('Steady-state initialization complete')
        
    def fix_initial_condition(blk):
        """
        Initial condition for material and enthalpy balance.

        Mass balance : Initial condition  is determined by
        fixing n-1 mole fraction and the total molar flowrate

        Energy balance :Initial condition  is determined by
        fixing  the temperature.

        """

        vap_comp = blk.config.vapor_side.property_package.component_list
        liq_comp = blk.config.liquid_side.property_package.component_list
        solute_comp_list = blk.config.liquid_side.property_package.solute_set

        for x in blk.vapor_phase.length_domain:
            if x != 0:
                blk.vapor_phase.properties[0, x].temperature.fix()
                blk.vapor_phase.properties[0, x].flow_mol.fix()
            for j in vap_comp:
                if (x != 0 and j not in solute_comp_list):
                    blk.vapor_phase.properties[0, x].mole_frac_comp[j].fix()
        for x in blk.liquid_phase.length_domain:
            if x != 1:
                blk.liquid_phase.properties[0, x].temperature.fix()
                blk.liquid_phase.properties[0, x].flow_mol.fix()
            for j in liq_comp:
                if (x != 1 and j not in solute_comp_list):
                    blk.liquid_phase.properties[0, x].mole_frac_comp[j].fix()

    def unfix_initial_condition(blk):
        """
        Function to unfix initial condition for material and enthalpy balance.

        """

        vap_comp = blk.config.vapor_side.property_package.component_list
        liq_comp = blk.config.liquid_side.property_package.component_list
        solute_comp_list = blk.config.liquid_side.property_package.solute_set

        for x in blk.vapor_phase.length_domain:
            if x != 0:
                blk.vapor_phase.properties[0, x].temperature.unfix()
                blk.vapor_phase.properties[0, x].flow_mol.unfix()
            for j in vap_comp:
                if (x != 0 and j not in solute_comp_list):
                    blk.vapor_phase.properties[0, x].mole_frac_comp[j].unfix()
        for x in blk.liquid_phase.length_domain:
            if x != 1:
                blk.liquid_phase.properties[0, x].temperature.unfix()
                blk.liquid_phase.properties[0, x].flow_mol.unfix()
            for j in liq_comp:
                if (x != 1 and j not in solute_comp_list):
                    blk.liquid_phase.properties[0, x].mole_frac_comp[j].unfix()
                    
    def make_steady_state_column_profile(blk):
        """
        Steady-state Plot function for Temperature and Solute Pressure profile.

        """

        normalised_column_height = [x for x in blk.vapor_phase.length_domain]
        simulation_time = [t for t in blk.flowsheet().time]

        # final time
        tf = simulation_time[-1]
        
        # solute list
        solute_comp_list = blk.config.liquid_side.property_package.solute_set
        solute_profile = []
        
        liquid_temperature_profile = []
        solute_comp_profile = []

        # APPEND RESULTS
        for j in solute_comp_list:
            for x in blk.vapor_phase.length_domain:
                x_liq = blk.liquid_phase.length_domain.at(blk.zi[x].value)
                solute_comp_profile.append(
                    value(1e-3 * blk.vapor_phase.properties[tf, x].pressure *
                          blk.vapor_phase.properties[tf, x].mole_frac_comp[j]))
                liquid_temperature_profile.append(
                    value(blk.liquid_phase.properties[tf, x_liq].temperature))
            solute_profile.append(solute_comp_profile)

        # plot properties
        fontsize = 18
        labelsize = 18
        fig = plt.figure(figsize=(9, 7))
        ax1 = fig.add_subplot(111)
        ax1.set_title('Steady-state column profile',
                      fontsize=16, fontweight='bold')

        # plot primary axis
        lab1 = ax1.plot(normalised_column_height, solute_profile[0],
                        linestyle='--', mec="b", mfc="None",
                        color='b', label='solute partial pressure [kPa]',
                        marker='o')

        ax1.tick_params(axis='y', labelcolor='b',
                        direction='in', labelsize=labelsize)
        ax1.tick_params(axis='x', direction='in', labelsize=labelsize)

        ax1.set_xlabel('Normalise column  height from bottom',
                       fontsize=fontsize)
        ax1.set_ylabel('P_solute  [ kPa]', color='b', fontweight='bold',
                       fontsize=fontsize)
        # plot secondary axis
        ax2 = ax1.twinx()
        lab2 = ax2.plot(normalised_column_height,
                        liquid_temperature_profile,
                        color='g',
                        linestyle='-',
                        label='Liquid temperature profile',
                        marker='s')
        ax2.set_ylabel('T$_{liq}$ [ K ] ', color='g', fontweight='bold',
                       fontsize=fontsize)
        ax2.tick_params(axis='y', labelcolor='g',
                        direction='in', labelsize=labelsize)

        # get the labels
        lab_1 = lab1 + lab2
        labels_1 = [lb.get_label() for lb in lab_1]
        ax1.legend(lab_1, labels_1, loc='lower center', fontsize=fontsize)
        fig.tight_layout()

        # show graph
        plt.show()

    def make_dynamic_column_profile(blk):
        """
        Dynamic Plot function for Temperature and Solute Pressure profile.

        """

        normalised_column_height = [x for x in blk.vapor_phase.length_domain]
        simulation_time = [t for t in blk.flowsheet().time]
        fluegas_flow = [value(blk.vapor_inlet.flow_mol[t])
                        for t in blk.flowsheet().time]

        # final time
        tf = simulation_time[-1]
        nf = len(simulation_time)

        # mid-time
        if nf % 2 == 0:
            tm = int(nf / 2)
        else:
            tm = int(nf / 2 + 1)

        solute_comp_list = blk.config.liquid_side.property_package.solute_set
        solute_profile_mid = []
        solute_profile_fin = []
        liquid_temperature_profile_mid = []
        liquid_temperature_profile_fin = []
        solute_comp_profile_mid = []
        solute_comp_profile_fin = []

        # APPEND RESULTS
        for j in solute_comp_list:
            for x in blk.vapor_phase.length_domain:
                x_liq = blk.liquid_phase.length_domain.at(blk.zi[x].value)
                solute_comp_profile_mid.append(
                    value(1e-3 * blk.vapor_phase.properties[tm, x].pressure *
                          blk.vapor_phase.properties[tm, x].mole_frac_comp[j]))
                solute_comp_profile_fin.append(
                    value(1e-3 * blk.vapor_phase.properties[tf, x].pressure *
                          blk.vapor_phase.properties[tf, x].mole_frac_comp[j]))
    
                liquid_temperature_profile_mid.append(
                    value(blk.liquid_phase.properties[tm, x_liq].temperature))
                liquid_temperature_profile_fin.append(
                    value(blk.liquid_phase.properties[tf, x_liq].temperature))
            solute_profile_mid.append(solute_comp_profile_mid)
            solute_profile_fin.append(solute_comp_profile_fin)

        # plot properties
        fontsize = 18
        labelsize = 18
        fig = plt.figure(figsize=(12, 7))
        ax1 = fig.add_subplot(211)
        ax1.set_title(
            'Column profile @ {0:6.2f} & {1:6.2f} sec'.format(tm, tf),
            fontsize=16, fontweight='bold')

        # plot primary axis
        lab1 = ax1.plot(normalised_column_height, solute_profile_mid[0],
                        linestyle='--', color='b',
                        label='Solute partial pressure [kPa] @ %d' % tm)
        lab2 = ax1.plot(normalised_column_height, solute_profile_fin[0],
                        linestyle='-', color='b',
                        label='Solute partial pressure [kPa] @ %d' % tf)

        ax1.tick_params(axis='y', labelcolor='b',
                        direction='in', labelsize=labelsize)
        ax1.tick_params(axis='x', direction='in', labelsize=labelsize)

        ax1.set_xlabel('Normalise column  height from bottom',
                       fontsize=fontsize)
        ax1.set_ylabel('P_solute  [ kPa]', color='b', fontweight='bold',
                       fontsize=fontsize)

        # plot secondary axis
        ax2 = ax1.twinx()
        lab3 = ax2.plot(
            normalised_column_height,
            liquid_temperature_profile_mid,
            color='g', linestyle='--',
            label='Liquid temperature profile @ {0:6.1f}'.format(tm))
        lab4 = ax2.plot(
            normalised_column_height,
            liquid_temperature_profile_fin,
            color='g', linestyle='-',
            label='Liquid temperature profile @ {0:6.1f}'.format(tf))
        ax2.set_ylabel('T$_{liq}$ [ K ] ', color='g', fontweight='bold',
                       fontsize=fontsize)
        ax2.tick_params(axis='y', labelcolor='g',
                        direction='in', labelsize=labelsize)
        # get the labels
        lab_1 = lab1 + lab2 + lab3 + lab4
        labels_1 = [lb.get_label() for lb in lab_1]
        ax1.legend(lab_1, labels_1, fontsize=fontsize)

        # plot flowgas flow
        ax3 = fig.add_subplot(212)
        ax3.plot(simulation_time, fluegas_flow,
                 linestyle='--', mec="g", mfc="None",
                 color='g', label='Fluegas flow [mol/s]',
                 marker='o')
        ax3.tick_params(labelsize=labelsize)
        ax3.set_xlabel('Simulation time (sec)', fontsize=fontsize)
        ax3.set_ylabel(' Fv  [ mol/s]', color='b', fontweight='bold',
                       fontsize=fontsize)
        ax3.legend(['Fluegas flow [mol/s]'], fontsize=fontsize)
        fig.tight_layout()
        plt.show()
Exemple #8
0
class MultiStart(object):
    """Solver wrapper that initializes at multiple starting points.

    # TODO: also return appropriate duals

    For theoretical underpinning, see
    https://www.semanticscholar.org/paper/How-many-random-restarts-are-enough-Dick-Wong/55b248b398a03dc1ac9a65437f88b835554329e0

    Keyword arguments below are specified for the ``solve`` function.

    """

    CONFIG = ConfigBlock("MultiStart")
    CONFIG.declare(
        "strategy",
        ConfigValue(
            default="rand",
            domain=In([
                "rand", "midpoint_guess_and_bound", "rand_guess_and_bound",
                "rand_distributed"
            ]),
            description="Specify the restart strategy. Defaults to rand.",
            doc="""Specify the restart strategy.

        - "rand": random choice between variable bounds
        - "midpoint_guess_and_bound": midpoint between current value and farthest bound
        - "rand_guess_and_bound": random choice between current value and farthest bound
        - "rand_distributed": random choice among evenly distributed values
        """))
    CONFIG.declare(
        "solver",
        ConfigValue(default="ipopt",
                    description="solver to use, defaults to ipopt"))
    CONFIG.declare(
        "solver_args",
        ConfigValue(
            default={},
            description="Dictionary of keyword arguments to pass to the solver."
        ))
    CONFIG.declare(
        "iterations",
        ConfigValue(
            default=10,
            description="Specify the number of iterations, defaults to 10. "
            "If -1 is specified, the high confidence stopping rule will be used"
        ))
    CONFIG.declare(
        "stopping_mass",
        ConfigValue(
            default=0.5,
            description="Maximum allowable estimated missing mass of optima.",
            doc="""Maximum allowable estimated missing mass of optima for the
        high confidence stopping rule, only used with the random strategy.
        The lower the parameter, the stricter the rule.
        Value bounded in (0, 1]."""))
    CONFIG.declare(
        "stopping_delta",
        ConfigValue(
            default=0.5,
            description=
            "1 minus the confidence level required for the stopping rule.",
            doc=
            """1 minus the confidence level required for the stopping rule for the
        high confidence stopping rule, only used with the random strategy.
        The lower the parameter, the stricter the rule.
        Value bounded in (0, 1]."""))
    CONFIG.declare(
        "suppress_unbounded_warning",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "True to suppress warning for skipping unbounded variables."))
    CONFIG.declare(
        "HCS_max_iterations",
        ConfigValue(
            default=1000,
            description=
            "Maximum number of iterations before interrupting the high confidence stopping rule."
        ))
    CONFIG.declare(
        "HCS_tolerance",
        ConfigValue(
            default=0,
            description=
            "Tolerance on HCS objective value equality. Defaults to Python float equality precision."
        ))

    __doc__ = add_docstring_list(__doc__, CONFIG)

    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 solve(self, model, **kwds):
        # initialize keyword args
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)

        # initialize the solver
        solver = SolverFactory(config.solver)

        # Model sense
        objectives = model.component_data_objects(Objective, active=True)
        obj = next(objectives, None)
        if next(objectives, None) is not None:
            raise RuntimeError(
                "Multistart solver is unable to handle model with multiple active objectives."
            )
        if obj is None:
            raise RuntimeError(
                "Multistart solver is unable to handle model with no active objective."
            )

        # store objective values and objective/result information for best
        # solution obtained
        objectives = []
        obj_sign = 1 if obj.sense == minimize else -1
        best_objective = float('inf') * obj_sign
        best_model = model
        best_result = None

        try:
            # create temporary variable list for value transfer
            tmp_var_list_name = unique_component_name(model, "_vars_list")
            setattr(
                model, tmp_var_list_name,
                list(model.component_data_objects(ctype=Var,
                                                  descend_into=True)))

            best_result = result = solver.solve(model, **config.solver_args)
            if (result.solver.status is SolverStatus.ok
                    and result.solver.termination_condition is tc.optimal):
                obj_val = value(model.obj.expr)
                best_objective = obj_val
                objectives.append(obj_val)
            num_iter = 0
            max_iter = config.iterations
            # if HCS rule is specified, reinitialize completely randomly until
            # rule specifies stopping
            using_HCS = config.iterations == -1
            HCS_completed = False
            if using_HCS:
                assert config.strategy == "rand", \
                    "High confidence stopping rule requires rand strategy."
                max_iter = config.HCS_max_iterations

            while num_iter < max_iter:
                if using_HCS and should_stop(objectives, config.stopping_mass,
                                             config.stopping_delta,
                                             config.HCS_tolerance):
                    HCS_completed = True
                    break
                num_iter += 1
                # at first iteration, solve the originally passed model
                m = model.clone() if num_iter > 1 else model
                reinitialize_variables(m, config)
                result = solver.solve(m, **config.solver_args)
                if (result.solver.status is SolverStatus.ok
                        and result.solver.termination_condition is tc.optimal):
                    obj_val = value(m.obj.expr)
                    objectives.append(obj_val)
                    if obj_val * obj_sign < obj_sign * best_objective:
                        # objective has improved
                        best_objective = obj_val
                        best_model = m
                        best_result = result
                if num_iter == 1:
                    # if it's the first iteration, set the best_model and
                    # best_result regardless of solution status in case the
                    # model is infeasible.
                    best_model = m
                    best_result = result

            if using_HCS and not HCS_completed:
                logger.warning(
                    "High confidence stopping rule was unable to complete "
                    "after %s iterations. To increase this limit, change the "
                    "HCS_max_iterations flag." % num_iter)

            # if no better result was found than initial solve, then return
            # that without needing to copy variables.
            if best_model is model:
                return best_result

            # reassign the given models vars to the new models vars
            orig_var_list = getattr(model, tmp_var_list_name)
            best_soln_var_list = getattr(best_model, tmp_var_list_name)
            for orig_var, new_var in zip(orig_var_list, best_soln_var_list):
                if not orig_var.is_fixed():
                    orig_var.value = new_var.value

            return best_result
        finally:
            # Remove temporary variable list
            delattr(model, tmp_var_list_name)

    def __enter__(self):
        return self

    def __exit__(self, t, v, traceback):
        pass
Exemple #9
0
class Electrodialysis0DData(UnitModelBlockData):
    """
    0D Electrodialysis Model
    """

    # 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(
        "operation_mode",
        ConfigValue(
            default="Constant_Current",
            domain=In(["Constant_Current", "Constant_Voltage"]),
            description="The electrical operation mode. To be selected between Constant Current and Constant Voltage",
        ),
    )

    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.}""",
        ),
    )

    # # TODO: Consider adding the EnergyBalanceType config using the following code
    '''
    CONFIG.declare("energy_balance_type", ConfigValue(
        default=EnergyBalanceType.none,
        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.}""",
        ),
    )

    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

        # Create essential sets.
        self.membrane_set = Set(initialize=["cem", "aem"])

        # Create unit model parameters and vars
        self.water_density = Param(
            initialize=1000,
            mutable=False,
            units=pyunits.kg * pyunits.m**-3,
            doc="density of water",
        )

        self.cell_pair_num = Var(
            initialize=1,
            domain=NonNegativeIntegers,
            bounds=(1, 10000),
            units=pyunits.dimensionless,
            doc="cell pair number in a stack",
        )

        # electrodialysis cell dimensional properties
        self.cell_width = Var(
            initialize=0.1,
            bounds=(1e-3, 1e2),
            units=pyunits.meter,
            doc="The width of the electrodialysis cell, denoted as b in the model description",
        )
        self.cell_length = Var(
            initialize=0.5,
            bounds=(1e-3, 1e2),
            units=pyunits.meter,
            doc="The length of the electrodialysis cell, denoted as l in the model description",
        )
        self.spacer_thickness = Var(
            initialize=0.0001,
            units=pyunits.meter,
            doc="The distance between the concecutive aem and cem",
        )

        # Material and Operational properties
        self.membrane_thickness = Var(
            self.membrane_set,
            initialize=0.0001,
            bounds=(1e-6, 1e-1),
            units=pyunits.meter,
            doc="Membrane thickness",
        )
        self.solute_diffusivity_membrane = Var(
            self.membrane_set,
            self.config.property_package.ion_set
            | self.config.property_package.solute_set,
            initialize=1e-10,
            bounds=(1e-16, 1e-6),
            units=pyunits.meter**2 * pyunits.second**-1,
            doc="Solute (ionic and neutral) diffusivity in the membrane phase",
        )
        self.ion_trans_number_membrane = Var(
            self.membrane_set,
            self.config.property_package.ion_set,
            bounds=(0, 1),
            units=pyunits.dimensionless,
            doc="Ion transference number in the membrane phase",
        )
        self.water_trans_number_membrane = Var(
            self.membrane_set,
            initialize=5,
            bounds=(0, 50),
            units=pyunits.dimensionless,
            doc="Transference number of water in membranes",
        )
        self.water_permeability_membrane = Var(
            self.membrane_set,
            initialize=1e-14,
            units=pyunits.meter * pyunits.second**-1 * pyunits.pascal**-1,
            doc="Water permeability coefficient",
        )
        self.membrane_surface_resistance = Var(
            self.membrane_set,
            initialize=2e-4,
            bounds=(1e-6, 1),
            units=pyunits.ohm * pyunits.meter**2,
            doc="Surface resistance of membrane",
        )
        self.electrodes_resistance = Var(
            initialize=0,
            bounds=(0, 100),
            domain=NonNegativeReals,
            units=pyunits.ohm * pyunits.meter**2,
            doc="areal resistance of TWO electrode compartments of a stack",
        )
        self.current = Var(
            self.flowsheet().config.time,
            initialize=1,
            bounds=(0, 1000),
            units=pyunits.amp,
            doc="Current across a cell-pair or stack",
        )
        self.voltage = Var(
            self.flowsheet().config.time,
            initialize=100,
            bounds=(0, 1000),
            units=pyunits.volt,
            doc="Voltage across a stack, declared under the 'Constant Voltage' mode only",
        )
        self.current_utilization = Var(
            initialize=1,
            bounds=(0, 1),
            units=pyunits.dimensionless,
            doc="The current utilization including water electro-osmosis and ion diffusion",
        )

        # Performance metrics
        self.current_efficiency = Var(
            self.flowsheet().config.time,
            initialize=0.9,
            bounds=(0, 1),
            units=pyunits.dimensionless,
            doc="The overall current efficiency for deionizaiton",
        )
        self.power_electrical = Var(
            self.flowsheet().config.time,
            initialize=1,
            bounds=(0, 12100),
            domain=NonNegativeReals,
            units=pyunits.watt,
            doc="Electrical power consumption of a stack",
        )
        self.specific_power_electrical = Var(
            self.flowsheet().config.time,
            initialize=10,
            bounds=(0, 1000),
            domain=NonNegativeReals,
            units=pyunits.kW * pyunits.hour * pyunits.meter**-3,
            doc="Diluate-volume-flow-rate-specific electrical power consumption",
        )
        # TODO: consider adding more performance as needed.

        # Fluxes Vars for constructing mass transfer terms
        self.elec_migration_flux_in = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1,
            doc="Molar flux_in of a component across the membrane driven by electrical migration",
        )
        self.elec_migration_flux_out = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1,
            doc="Molar flux_out of a component across the membrane driven by electrical migration",
        )
        self.nonelec_flux_in = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1,
            doc="Molar flux_in of a component across the membrane driven by non-electrical forces",
        )
        self.nonelec_flux_out = Var(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            units=pyunits.mole * pyunits.meter**-2 * pyunits.second**-1,
            doc="Molar flux_out of a component across the membrane driven by non-electrical forces",
        )

        # Build control volume for the dilute channel
        self.diluate_channel = ControlVolume0DBlock(
            default={
                "dynamic": False,
                "has_holdup": False,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
            }
        )
        self.diluate_channel.add_state_blocks(has_phase_equilibrium=False)
        self.diluate_channel.add_material_balances(
            balance_type=self.config.material_balance_type, has_mass_transfer=True
        )
        self.diluate_channel.add_momentum_balances(
            balance_type=self.config.momentum_balance_type, has_pressure_change=False
        )
        # # TODO: Consider adding energy balances

        # Build control volume for the concentrate channel
        self.concentrate_channel = ControlVolume0DBlock(
            default={
                "dynamic": False,
                "has_holdup": False,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_package_args,
            }
        )
        self.concentrate_channel.add_state_blocks(has_phase_equilibrium=False)
        self.concentrate_channel.add_material_balances(
            balance_type=self.config.material_balance_type, has_mass_transfer=True
        )
        self.concentrate_channel.add_momentum_balances(
            balance_type=self.config.momentum_balance_type, has_pressure_change=False
        )
        # # TODO: Consider adding energy balances

        # Add ports (creates inlets and outlets for each channel)
        self.add_inlet_port(name="inlet_diluate", block=self.diluate_channel)
        self.add_outlet_port(name="outlet_diluate", block=self.diluate_channel)
        self.add_inlet_port(name="inlet_concentrate", block=self.concentrate_channel)
        self.add_outlet_port(name="outlet_concentrate", block=self.concentrate_channel)

        # Build Constraints
        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            doc="Current-Voltage relationship",
        )
        def eq_current_voltage_relation(self, t, p):
            surface_resistance_cp = (
                self.membrane_surface_resistance["aem"]
                + self.membrane_surface_resistance["cem"]
                + self.spacer_thickness
                / (
                    0.5
                    * (
                        self.concentrate_channel.properties_in[
                            t
                        ].electrical_conductivity_phase[p]
                        + self.concentrate_channel.properties_out[
                            t
                        ].electrical_conductivity_phase[p]
                        + self.diluate_channel.properties_in[
                            t
                        ].electrical_conductivity_phase[p]
                        + self.diluate_channel.properties_out[
                            t
                        ].electrical_conductivity_phase[p]
                    )
                )
            )
            return (
                self.current[t]
                * (
                    surface_resistance_cp * self.cell_pair_num
                    + self.electrodes_resistance
                )
                == self.voltage[t] * self.cell_width * self.cell_length
            )

        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Equation for electrical migration flux_in",
        )
        def eq_elec_migration_flux_in(self, t, p, j):
            if j == "H2O":
                return self.elec_migration_flux_in[t, p, j] == (
                    self.water_trans_number_membrane["cem"]
                    + self.water_trans_number_membrane["aem"]
                ) * (
                    self.current[t]
                    / (self.cell_width * self.cell_length)
                    / Constants.faraday_constant
                )
            elif j in self.config.property_package.ion_set:
                return self.elec_migration_flux_in[t, p, j] == (
                    self.ion_trans_number_membrane["cem", j]
                    - self.ion_trans_number_membrane["aem", j]
                ) * (
                    self.current_utilization
                    * self.current[t]
                    / (self.cell_width * self.cell_length)
                ) / (
                    self.config.property_package.charge_comp[j]
                    * Constants.faraday_constant
                )
            else:
                return self.elec_migration_flux_out[t, p, j] == 0

        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Equation for electrical migration flux_out",
        )
        def eq_elec_migration_flux_out(self, t, p, j):
            if j == "H2O":
                return self.elec_migration_flux_out[t, p, j] == (
                    self.water_trans_number_membrane["cem"]
                    + self.water_trans_number_membrane["aem"]
                ) * (
                    self.current[t]
                    / (self.cell_width * self.cell_length)
                    / Constants.faraday_constant
                )
            elif j in self.config.property_package.ion_set:
                return self.elec_migration_flux_out[t, p, j] == (
                    self.ion_trans_number_membrane["cem", j]
                    - self.ion_trans_number_membrane["aem", j]
                ) * (
                    self.current_utilization
                    * self.current[t]
                    / (self.cell_width * self.cell_length)
                ) / (
                    self.config.property_package.charge_comp[j]
                    * Constants.faraday_constant
                )
            else:
                return self.elec_migration_flux_out[t, p, j] == 0

        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Equation for non-electrical flux_in",
        )
        def eq_nonelec_flux_in(self, t, p, j):
            if j == "H2O":
                return self.nonelec_flux_in[
                    t, p, j
                ] == self.water_density / self.config.property_package.mw_comp[j] * (
                    self.water_permeability_membrane["cem"]
                    + self.water_permeability_membrane["aem"]
                ) * (
                    self.concentrate_channel.properties_in[t].pressure_osm_phase[p]
                    - self.diluate_channel.properties_in[t].pressure_osm_phase[p]
                )
            else:
                return self.nonelec_flux_in[t, p, j] == -(
                    self.solute_diffusivity_membrane["cem", j]
                    / self.membrane_thickness["cem"]
                    + self.solute_diffusivity_membrane["aem", j]
                    / self.membrane_thickness["aem"]
                ) * (
                    self.concentrate_channel.properties_in[t].conc_mol_phase_comp[p, j]
                    - self.diluate_channel.properties_in[t].conc_mol_phase_comp[p, j]
                )

        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Equation for non-electrical flux_out",
        )
        def eq_nonelec_flux_out(self, t, p, j):
            if j == "H2O":
                return self.nonelec_flux_out[
                    t, p, j
                ] == self.water_density / self.config.property_package.mw_comp[j] * (
                    self.water_permeability_membrane["cem"]
                    + self.water_permeability_membrane["aem"]
                ) * (
                    self.concentrate_channel.properties_out[t].pressure_osm_phase[p]
                    - self.diluate_channel.properties_out[t].pressure_osm_phase[p]
                )
            else:
                return self.nonelec_flux_out[t, p, j] == -(
                    self.solute_diffusivity_membrane["cem", j]
                    / self.membrane_thickness["cem"]
                    + self.solute_diffusivity_membrane["aem", j]
                    / self.membrane_thickness["aem"]
                ) * (
                    self.concentrate_channel.properties_out[t].conc_mol_phase_comp[p, j]
                    - self.diluate_channel.properties_out[t].conc_mol_phase_comp[p, j]
                )

        # Add constraints for mass transfer terms (diluate_channel)
        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Mass transfer term for the diluate channel",
        )
        def eq_mass_transfer_term_diluate(self, t, p, j):
            return self.diluate_channel.mass_transfer_term[t, p, j] == -0.5 * (
                self.elec_migration_flux_in[t, p, j]
                + self.elec_migration_flux_out[t, p, j]
                + self.nonelec_flux_in[t, p, j]
                + self.nonelec_flux_out[t, p, j]
            ) * (self.cell_width * self.cell_length)

        # Add constraints for mass transfer terms (concentrate_channel)
        @self.Constraint(
            self.flowsheet().config.time,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            doc="Mass transfer term for the concentrate channel",
        )
        def eq_mass_transfer_term_concentrate(self, t, p, j):
            return self.concentrate_channel.mass_transfer_term[t, p, j] == 0.5 * (
                self.elec_migration_flux_in[t, p, j]
                + self.elec_migration_flux_out[t, p, j]
                + self.nonelec_flux_in[t, p, j]
                + self.nonelec_flux_out[t, p, j]
            ) * (self.cell_width * self.cell_length)

        # Add isothermal condition
        @self.Constraint(
            self.flowsheet().config.time,
            doc="Isothermal condition for the diluate channel",
        )
        def eq_isothermal_diluate(self, t):
            return (
                self.diluate_channel.properties_in[t].temperature
                == self.diluate_channel.properties_out[t].temperature
            )

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Isothermal condition for the concentrate channel",
        )
        def eq_isothermal_concentrate(self, t):
            return (
                self.concentrate_channel.properties_in[t].temperature
                == self.concentrate_channel.properties_out[t].temperature
            )

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Electrical power consumption of a stack",
        )
        def eq_power_electrical(self, t):
            return self.power_electrical[t] == self.current[t] * self.voltage[t]

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Diluate_volume_flow_rate_specific electrical power consumption of a stack",
        )
        def eq_specific_power_electrical(self, t):
            return (
                pyunits.convert(
                    self.specific_power_electrical[t],
                    pyunits.watt * pyunits.second * pyunits.meter**-3,
                )
                * self.diluate_channel.properties_out[t].flow_vol_phase["Liq"]
                == self.current[t] * self.voltage[t]
            )

        @self.Constraint(
            self.flowsheet().config.time,
            doc="Overall current efficiency evaluation",
        )
        def eq_current_efficiency(self, t):
            return (
                self.current_efficiency[t] * self.current[t]
                == sum(
                    self.diluate_channel.properties_in[t].flow_mol_phase_comp["Liq", j]
                    * self.config.property_package.charge_comp[j]
                    - self.diluate_channel.properties_out[t].flow_mol_phase_comp[
                        "Liq", j
                    ]
                    * self.config.property_package.charge_comp[j]
                    for j in self.config.property_package.cation_set
                )
                * Constants.faraday_constant
            )

    # 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)

        # ---------------------------------------------------------------------

        # Set the outlet has the same intial condition of the inlet.
        for k in blk.keys():
            for j in blk[k].config.property_package.component_list:
                blk[k].diluate_channel.properties_out[0].flow_mol_phase_comp[
                    "Liq", j
                ] = value(
                    blk[k]
                    .diluate_channel.properties_in[0]
                    .flow_mol_phase_comp["Liq", j]
                )
                blk[k].concentrate_channel.properties_out[0].flow_mol_phase_comp[
                    "Liq", j
                ] = value(
                    blk[k]
                    .concentrate_channel.properties_in[0]
                    .flow_mol_phase_comp["Liq", j]
                )
        # Initialize diluate_channel block
        flags_diluate = blk.diluate_channel.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
            hold_state=True,
        )
        init_log.info_high("Initialization Step 1 Complete.")
        # ---------------------------------------------------------------------
        # Initialize concentrate_side block
        flags_concentrate = blk.concentrate_channel.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,  # inlet var
            hold_state=True,
        )
        init_log.info_high("Initialization Step 2 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 3 {}.".format(idaeslog.condition(res)))
        # ---------------------------------------------------------------------
        # Release state
        blk.diluate_channel.release_state(flags_diluate, outlvl)
        init_log.info("Initialization Complete: {}".format(idaeslog.condition(res)))
        blk.concentrate_channel.release_state(flags_concentrate, outlvl)
        init_log.info("Initialization Complete: {}".format(idaeslog.condition(res)))

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        # Var scaling
        # The following Vars' sf are allowed to be provided by users or set by default.
        if (
            iscale.get_scaling_factor(self.solute_diffusivity_membrane, warning=True)
            is None
        ):
            iscale.set_scaling_factor(self.solute_diffusivity_membrane, 1e10)
        if iscale.get_scaling_factor(self.membrane_thickness, warning=True) is None:
            iscale.set_scaling_factor(self.membrane_thickness, 1e4)
        if (
            iscale.get_scaling_factor(self.water_permeability_membrane, warning=True)
            is None
        ):
            iscale.set_scaling_factor(self.water_permeability_membrane, 1e14)
        if iscale.get_scaling_factor(self.cell_length, warning=True) is None:
            iscale.set_scaling_factor(self.cell_length, 1e1)
        if iscale.get_scaling_factor(self.cell_width, warning=True) is None:
            iscale.set_scaling_factor(self.cell_width, 1e1)
        if iscale.get_scaling_factor(self.spacer_thickness, warning=True) is None:
            iscale.set_scaling_factor(self.spacer_thickness, 1e4)
        if (
            iscale.get_scaling_factor(self.membrane_surface_resistance, warning=True)
            is None
        ):
            iscale.set_scaling_factor(self.membrane_surface_resistance, 1e4)
        if iscale.get_scaling_factor(self.electrodes_resistance, warning=True) is None:
            iscale.set_scaling_factor(self.electrodes_resistance, 1e4)
        if iscale.get_scaling_factor(self.current, warning=True) is None:
            iscale.set_scaling_factor(self.current, 1)
        if iscale.get_scaling_factor(self.voltage, warning=True) is None:
            iscale.set_scaling_factor(self.voltage, 1e-1)
        # The folloing Vars are built for constructing constraints and their sf are computed from other Vars.
        iscale.set_scaling_factor(
            self.elec_migration_flux_in,
            iscale.get_scaling_factor(self.current)
            * iscale.get_scaling_factor(self.cell_length) ** -1
            * iscale.get_scaling_factor(self.cell_width) ** -1
            * 1e5,
        )
        iscale.set_scaling_factor(
            self.elec_migration_flux_out,
            iscale.get_scaling_factor(self.current)
            * iscale.get_scaling_factor(self.cell_length) ** -1
            * iscale.get_scaling_factor(self.cell_width) ** -1
            * 1e5,
        )
        iscale.set_scaling_factor(
            self.power_electrical,
            iscale.get_scaling_factor(self.current)
            * iscale.get_scaling_factor(self.voltage),
        )
        for ind, c in self.specific_power_electrical.items():
            iscale.set_scaling_factor(
                self.specific_power_electrical[ind],
                3.6e6
                * iscale.get_scaling_factor(self.current[ind])
                * iscale.get_scaling_factor(self.voltage[ind])
                * iscale.get_scaling_factor(
                    self.diluate_channel.properties_out[ind].flow_vol_phase["Liq"]
                )
                ** -1,
            )

        # Constraint scaling
        for ind, c in self.eq_current_voltage_relation.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.membrane_surface_resistance)
            )
        for ind, c in self.eq_power_electrical.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.power_electrical)
            )
        for ind, c in self.eq_specific_power_electrical.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.specific_power_electrical[ind])
            )

        for ind, c in self.eq_elec_migration_flux_in.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.elec_migration_flux_in)
            )
        for ind, c in self.eq_elec_migration_flux_out.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.elec_migration_flux_out)
            )
        for ind, c in self.eq_nonelec_flux_in.items():
            if ind[2] == "H2O":
                sf = (
                    1e-3
                    * 0.018
                    * iscale.get_scaling_factor(self.water_permeability_membrane)
                    * iscale.get_scaling_factor(
                        self.concentrate_channel.properties_in[
                            ind[0]
                        ].pressure_osm_phase[ind[1]]
                    )
                )
            sf = (
                iscale.get_scaling_factor(self.solute_diffusivity_membrane)
                / iscale.get_scaling_factor(self.membrane_thickness)
                * iscale.get_scaling_factor(
                    self.concentrate_channel.properties_in[ind[0]].conc_mol_phase_comp[
                        ind[1], ind[2]
                    ]
                )
            )
            iscale.set_scaling_factor(self.nonelec_flux_in[ind], sf)
            iscale.constraint_scaling_transform(c, sf)
        for ind, c in self.eq_nonelec_flux_out.items():
            if ind[2] == "H2O":
                sf = (
                    1e-3
                    * 0.018
                    * iscale.get_scaling_factor(self.water_permeability_membrane)
                    * iscale.get_scaling_factor(
                        self.concentrate_channel.properties_out[
                            ind[0]
                        ].pressure_osm_phase[ind[1]]
                    )
                )
            else:
                sf = (
                    iscale.get_scaling_factor(self.solute_diffusivity_membrane)
                    / iscale.get_scaling_factor(self.membrane_thickness)
                    * iscale.get_scaling_factor(
                        self.concentrate_channel.properties_out[
                            ind[0]
                        ].conc_mol_phase_comp[ind[1], ind[2]]
                    )
                )

            iscale.set_scaling_factor(self.nonelec_flux_out[ind], sf)
            iscale.constraint_scaling_transform(c, sf)
        for ind, c in self.eq_mass_transfer_term_diluate.items():
            iscale.constraint_scaling_transform(
                c,
                min(
                    iscale.get_scaling_factor(self.elec_migration_flux_in[ind]),
                    iscale.get_scaling_factor(
                        self.nonelec_flux_in[ind], self.elec_migration_flux_out[ind]
                    ),
                    iscale.get_scaling_factor(self.nonelec_flux_out[ind]),
                ),
            )
        for ind, c in self.eq_mass_transfer_term_concentrate.items():
            iscale.constraint_scaling_transform(
                c,
                min(
                    iscale.get_scaling_factor(self.elec_migration_flux_in[ind]),
                    iscale.get_scaling_factor(
                        self.nonelec_flux_in[ind], self.elec_migration_flux_out[ind]
                    ),
                    iscale.get_scaling_factor(self.nonelec_flux_out[ind]),
                ),
            )

        for ind, c in self.eq_power_electrical.items():
            iscale.constraint_scaling_transform(
                c,
                iscale.get_scaling_factor(self.power_electrical[ind]),
            )

        for ind, c in self.eq_specific_power_electrical.items():
            iscale.constraint_scaling_transform(
                c,
                iscale.get_scaling_factor(self.specific_power_electrical[ind])
                * iscale.get_scaling_factor(
                    self.diluate_channel.properties_out[ind].flow_vol_phase["Liq"]
                ),
            )
        for ind, c in self.eq_current_efficiency.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(self.current[ind])
            )

        for ind, c in self.eq_isothermal_diluate.items():
            iscale.constraint_scaling_transform(
                c, self.diluate_channel.properties_in[ind].temperature
            )
        for ind, c in self.eq_isothermal_concentrate.items():
            iscale.constraint_scaling_transform(
                c, self.concentrate_channel.properties_in[ind].temperature
            )

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(
            {
                "Diluate Channel Inlet": self.inlet_diluate,
                "Concentrate Channel Inlet": self.inlet_concentrate,
                "Diluate Channel Outlet": self.outlet_diluate,
                "Concentrate Channel Outlet": self.outlet_concentrate,
            },
            time_point=time_point,
        )

    def _get_performance_contents(self, time_point=0):
        return {
            "vars": {
                "Electrical power consumption(Watt)": self.power_electrical[time_point],
                "Specific electrical power consumption (kWh/m**3)": self.specific_power_electrical[
                    time_point
                ],
                "Current efficiency for deionzation": self.current_efficiency[
                    time_point
                ],
            },
            "exprs": {},
            "params": {},
        }
Exemple #10
0
def pyros_config():
    CONFIG = ConfigDict('PyROS')

    # ================================================
    # === Options common to all solvers
    # ================================================
    CONFIG.declare(
        'time_limit',
        ConfigValue(
            default=None,
            domain=NonNegativeFloat,
            description="Optional. Default = None. "
            "Total allotted time for the execution of the PyROS solver in seconds "
            "(includes time spent in sub-solvers). 'None' is no time limit."))
    CONFIG.declare(
        'keepfiles',
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "Optional. Default = False. Whether or not to write files of sub-problems for use in debugging. "
            "Must be paired with a writable directory supplied via ``subproblem_file_directory``."
        ))
    CONFIG.declare(
        'tee',
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "Optional. Default = False. Sets the ``tee`` for all sub-solvers utilized."
        ))
    CONFIG.declare(
        'load_solution',
        ConfigValue(
            default=True,
            domain=bool,
            description="Optional. Default = True. "
            "Whether or not to load the final solution of PyROS into the model object."
        ))

    # ================================================
    # === Required User Inputs
    # ================================================
    CONFIG.declare(
        "first_stage_variables",
        ConfigValue(
            default=[],
            domain=InputDataStandardizer(Var, _VarData),
            description=
            "Required. List of ``Var`` objects referenced in ``model`` representing the design variables."
        ))
    CONFIG.declare(
        "second_stage_variables",
        ConfigValue(
            default=[],
            domain=InputDataStandardizer(Var, _VarData),
            description=
            "Required. List of ``Var`` referenced in ``model`` representing the control variables."
        ))
    CONFIG.declare(
        "uncertain_params",
        ConfigValue(
            default=[],
            domain=InputDataStandardizer(Param, _ParamData),
            description=
            "Required. List of ``Param`` referenced in ``model`` representing the uncertain parameters. MUST be ``mutable``. "
            "Assumes entries are provided in consistent order with the entries of 'nominal_uncertain_param_vals' input."
        ))
    CONFIG.declare(
        "uncertainty_set",
        ConfigValue(
            default=None,
            domain=uncertainty_sets,
            description=
            "Required. ``UncertaintySet`` object representing the uncertainty space "
            "that the final solutions will be robust against."))
    CONFIG.declare(
        "local_solver",
        ConfigValue(
            default=None,
            domain=SolverResolvable(),
            description=
            "Required. ``Solver`` object to utilize as the primary local NLP solver."
        ))
    CONFIG.declare(
        "global_solver",
        ConfigValue(
            default=None,
            domain=SolverResolvable(),
            description=
            "Required. ``Solver`` object to utilize as the primary global NLP solver."
        ))
    # ================================================
    # === Optional User Inputs
    # ================================================
    CONFIG.declare(
        "objective_focus",
        ConfigValue(
            default=ObjectiveType.nominal,
            domain=ValidEnum(ObjectiveType),
            description=
            "Optional. Default = ``ObjectiveType.nominal``. Choice of objective function to optimize in the master problems. "
            "Choices are: ``ObjectiveType.worst_case``, ``ObjectiveType.nominal``. See Note for details."
        ))
    CONFIG.declare(
        "nominal_uncertain_param_vals",
        ConfigValue(
            default=[],
            domain=list,
            description=
            "Optional. Default = deterministic model ``Param`` values. List of nominal values for all uncertain parameters. "
            "Assumes entries are provided in consistent order with the entries of ``uncertain_params`` input."
        ))
    CONFIG.declare(
        "decision_rule_order",
        ConfigValue(
            default=0,
            domain=In([0, 1, 2]),
            description=
            "Optional. Default = 0. Order of decision rule functions for handling second-stage variable recourse. "
            "Choices are: '0' for constant recourse (a.k.a. static approximation), '1' for affine recourse "
            "(a.k.a. affine decision rules), '2' for quadratic recourse."))
    CONFIG.declare(
        "solve_master_globally",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "Optional. Default = False. 'True' for the master problems to be solved with the user-supplied global solver(s); "
            "or 'False' for the master problems to be solved with the user-supplied local solver(s). "
        ))
    CONFIG.declare(
        "max_iter",
        ConfigValue(
            default=-1,
            domain=PositiveIntOrMinusOne,
            description=
            "Optional. Default = -1. Iteration limit for the GRCS algorithm. '-1' is no iteration limit."
        ))
    CONFIG.declare(
        "robust_feasibility_tolerance",
        ConfigValue(
            default=1e-4,
            domain=NonNegativeFloat,
            description=
            "Optional. Default = 1e-4. Relative tolerance for assessing robust feasibility violation during separation phase."
        ))
    CONFIG.declare(
        "separation_priority_order",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "Optional. Default = {}. Dictionary mapping inequality constraint names to positive integer priorities for separation. "
            "Constraints not referenced in the dictionary assume a priority of 0 (lowest priority)."
        ))
    CONFIG.declare(
        "progress_logger",
        ConfigValue(
            default="pyomo.contrib.pyros",
            domain=a_logger,
            description=
            "Optional. Default = \"pyomo.contrib.pyros\". The logger object to use for reporting."
        ))
    CONFIG.declare(
        "backup_local_solvers",
        ConfigValue(
            default=[],
            domain=SolverResolvable(),
            description=
            "Optional. Default = []. List of additional ``Solver`` objects to utilize as backup "
            "whenever primary local NLP solver fails to identify solution to a sub-problem."
        ))
    CONFIG.declare(
        "backup_global_solvers",
        ConfigValue(
            default=[],
            domain=SolverResolvable(),
            description=
            "Optional. Default = []. List of additional ``Solver`` objects to utilize as backup "
            "whenever primary global NLP solver fails to identify solution to a sub-problem."
        ))
    CONFIG.declare(
        "subproblem_file_directory",
        ConfigValue(
            default=None,
            domain=str,
            description=
            "Optional. Path to a directory where subproblem files and "
            "logs will be written in the case that a subproblem fails to solve."
        ))
    # ================================================
    # === Advanced Options
    # ================================================
    CONFIG.declare(
        "bypass_local_separation",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "This is an advanced option. Default = False. 'True' to only use global solver(s) during separation; "
            "'False' to use local solver(s) at intermediate separations, "
            "using global solver(s) only before termination to certify robust feasibility. "
        ))
    CONFIG.declare(
        "bypass_global_separation",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "This is an advanced option. Default = False. 'True' to only use local solver(s) during separation; "
            "however, robustness of the final result will not be guaranteed. Use to expedite PyROS run when "
            "global solver(s) cannot (efficiently) solve separation problems.")
    )
    CONFIG.declare(
        "p_robustness",
        ConfigValue(
            default={},
            domain=dict,
            description=
            "This is an advanced option. Default = {}. Whether or not to add p-robustness constraints to the master problems. "
            "If the dictionary is empty (default), then p-robustness constraints are not added. "
            "See Note for how to specify arguments."))

    return CONFIG
class PressureChangerData(UnitModelBlockData):
    """
    Standard Compressor/Expander Unit Model Class
    """

    CONFIG = UnitModelBlockData.CONFIG()

    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}""",
        ),
    )
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}""",
        ),
    )
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be
constructed, **default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""",
        ),
    )
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Phase equilibrium construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed, **default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}""",
        ),
    )
    CONFIG.declare(
        "compressor",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description="Compressor flag",
            doc="""Indicates whether this unit should be considered a
            compressor (True (default), pressure increase) or an expander
            (False, pressure decrease).""",
        ),
    )
    CONFIG.declare(
        "thermodynamic_assumption",
        ConfigValue(
            default=ThermodynamicAssumption.isothermal,
            domain=In(ThermodynamicAssumption),
            description="Thermodynamic assumption to use",
            doc="""Flag to set the thermodynamic assumption to use for the unit.
                - ThermodynamicAssumption.isothermal (default)
                - ThermodynamicAssumption.isentropic
                - ThermodynamicAssumption.pump
                - ThermodynamicAssumption.adiabatic""",
        ),
    )
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc="""Property parameter object used to define property
calculations, **default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}""",
        ),
    )
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc="""A ConfigBlock with arguments to be passed to a property
block(s) and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}""",
        ),
    )
    CONFIG.declare(
        "support_isentropic_performance_curves",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            doc="Include a block for performance curves, configure via"
                " isentropic_performance_curves.",
        ),
    )
    CONFIG.declare(
        "isentropic_performance_curves",
        IsentropicPerformanceCurveData.CONFIG(),
        # doc included in IsentropicPerformanceCurveData
    )

    def build(self):
        """

        Args:
            None

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

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

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

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

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

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

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

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

        # Set Unit Geometry and holdup Volume
        if self.config.has_holdup is True:
            self.volume = Reference(self.control_volume.volume[:])

        # Construct performance equations
        # Set references to balance terms at unit level
        # Add Work transfer variable 'work'
        self.work_mechanical = Reference(self.control_volume.work[:])

        # Add Momentum balance variable 'deltaP'
        self.deltaP = Reference(self.control_volume.deltaP[:])

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

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

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

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

        Args:
            None

        Returns:
            None
        """
        units_meta = self.config.property_package.get_metadata()

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

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

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

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

        Args:
            None

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

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

        Args:
            None

        Returns:
            None
        """
        @self.Constraint(self.flowsheet().config.time)
        def zero_work_equation(b, t):
            return self.control_volume.work[t] == 0

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

        Args:
            None

        Returns:
            None
        """
        units_meta = self.config.property_package.get_metadata()

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

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

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

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

        # This assumes isentropic composition is the same as outlet
        self.add_state_material_balances(self.config.material_balance_type,
                                         self.properties_isentropic,
                                         self.control_volume.properties_out)

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

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

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

        if self.config.support_isentropic_performance_curves:
            self.performance_curve = IsentropicPerformanceCurve(
                default=self.config.isentropic_performance_curves)

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

        Args:
            None

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

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

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

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

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

        Returns:
            None
        """
        # if costing block exists, deactivate
        try:
            blk.costing.deactivate()
        except AttributeError:
            pass

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

        # Call initialization routine
        if routine is ThermodynamicAssumption.isentropic:
            blk.init_isentropic(
                state_args=state_args,
                outlvl=outlvl,
                solver=solver,
                optarg=optarg
            )
        elif routine is ThermodynamicAssumption.adiabatic:
            blk.init_adiabatic(
                state_args=state_args,
                outlvl=outlvl,
                solver=solver,
                optarg=optarg
            )
        else:
            # Call the general initialization routine in UnitModelBlockData
            super().initialize(
                state_args=state_args,
                outlvl=outlvl,
                solver=solver,
                optarg=optarg
            )
        # if costing block exists, activate
        try:
            blk.costing.activate()
            costing.initialize(blk.costing)
        except AttributeError:
            pass

    def init_adiabatic(blk, state_args, outlvl, solver, optarg):
        """
        Initialization routine for adiabatic pressure changers.

        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={})
            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")

        # Create solver
        opt = get_solver(solver, optarg)

        cv = blk.control_volume
        t0 = blk.flowsheet().config.time.first()
        state_args_out = {}

        if state_args is None:
            state_args = {}
            state_dict = (
                cv.properties_in[t0].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

        # Get initialisation guesses for outlet and isentropic states
        for k in state_args:
            if k == "pressure" and k not in state_args_out:
                # Work out how to estimate outlet pressure
                if cv.properties_out[t0].pressure.fixed:
                    # Fixed outlet pressure, use this value
                    state_args_out[k] = value(
                        cv.properties_out[t0].pressure)
                elif blk.deltaP[t0].fixed:
                    state_args_out[k] = value(
                        state_args[k] + blk.deltaP[t0])
                elif blk.ratioP[t0].fixed:
                    state_args_out[k] = value(
                        state_args[k] * blk.ratioP[t0])
                else:
                    # Not obvious what to do, use inlet state
                    state_args_out[k] = state_args[k]
            elif k not in state_args_out:
                state_args_out[k] = state_args[k]

        # Initialize state blocks
        flags = cv.properties_in.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=True,
            state_args=state_args,
        )
        cv.properties_out.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=False,
            state_args=state_args_out,
        )
        init_log.info_high("Initialization Step 1 Complete.")

        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)))

        # ---------------------------------------------------------------------
        # 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 3 {}."
                           .format(idaeslog.condition(res)))

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

    def init_isentropic(blk, state_args, outlvl, solver, optarg):
        """
        Initialization routine for isentropic pressure changers.

        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={})
            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")

        # Create solver
        opt = get_solver(solver, optarg)

        cv = blk.control_volume
        t0 = blk.flowsheet().config.time.first()
        state_args_out = {}

        # performance curves exist and are active so initialize with them
        activate_performance_curves = (
            hasattr(blk, "performance_curve") and
            blk.performance_curve.has_constraints() and
            blk.performance_curve.active)
        if activate_performance_curves:
            blk.performance_curve.deactivate()
            # The performance curves will provide (maybe indirectly) efficency
            # and/or pressure ratio. To get through the standard isentropic
            # pressure changer init, we'll see if the user provided a guess for
            # pressure ratio or isentropic efficency and fix them if need. If
            # not fixed and no guess provided, fill in something reasonable
            # until the performance curves are turned on.
            unfix_eff = {}
            unfix_ratioP = {}
            for t in blk.flowsheet().config.time:
                if not (blk.ratioP[t].fixed or  blk.deltaP[t].fixed or
                    cv.properties_out[t].pressure.fixed):
                    if blk.config.compressor:
                        if not (value(blk.ratioP[t]) >= 1.01 and
                            value(blk.ratioP[t]) <= 50):
                            blk.ratioP[t] = 1.8
                    else:
                        if not (value(blk.ratioP[t]) >= 0.01 and
                            value(blk.ratioP[t]) <= 0.999):
                            blk.ratioP[t] = 0.7
                    blk.ratioP[t].fix()
                    unfix_ratioP[t] = True
                if not blk.efficiency_isentropic[t].fixed:
                    if not (value(blk.efficiency_isentropic[t]) >= 0.05 and
                        value(blk.efficiency_isentropic[t]) <= 1.0):
                        blk.efficiency_isentropic[t] = 0.8
                    blk.efficiency_isentropic[t].fix()
                    unfix_eff[t] = True

        if state_args is None:
            state_args = {}
            state_dict = (
                cv.properties_in[t0].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

        # Get initialisation guesses for outlet and isentropic states
        for k in state_args:
            if k == "pressure" and k not in state_args_out:
                # Work out how to estimate outlet pressure
                if cv.properties_out[t0].pressure.fixed:
                    # Fixed outlet pressure, use this value
                    state_args_out[k] = value(
                        cv.properties_out[t0].pressure)
                elif blk.deltaP[t0].fixed:
                    state_args_out[k] = value(
                        state_args[k] + blk.deltaP[t0])
                elif blk.ratioP[t0].fixed:
                    state_args_out[k] = value(
                        state_args[k] * blk.ratioP[t0])
                else:
                    # Not obvious what to do, use inlet state
                    state_args_out[k] = state_args[k]
            elif k not in state_args_out:
                state_args_out[k] = state_args[k]

        # Initialize state blocks
        flags = cv.properties_in.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=True,
            state_args=state_args,
        )
        cv.properties_out.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            hold_state=False,
            state_args=state_args_out,
        )

        init_log.info_high("Initialization Step 1 Complete.")
        # ---------------------------------------------------------------------
        # Initialize Isentropic block

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

        init_log.info_high("Initialization Step 2 Complete.")

        # ---------------------------------------------------------------------
        # Solve for isothermal conditions
        if isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].temperature,
            Var,
        ):
            blk.properties_isentropic[:].temperature.fix()
        elif isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].enth_mol,
            Var,
        ):
            blk.properties_isentropic[:].enth_mol.fix()
        elif isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].temperature,
            Expression,
        ):
            def tmp_rule(b, t):
                return blk.properties_isentropic[t].temperature == \
                    blk.control_volume.properties_in[t].temperature
            blk.tmp_init_constraint = Constraint(
                blk.flowsheet().config.time, rule=tmp_rule)

        blk.isentropic.deactivate()

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

        if isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].temperature,
            Var,
        ):
            blk.properties_isentropic[:].temperature.unfix()
        elif isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].enth_mol,
            Var,
        ):
            blk.properties_isentropic[:].enth_mol.unfix()
        elif isinstance(
            blk.properties_isentropic[
                blk.flowsheet().config.time.first()].temperature,
            Expression,
        ):
            blk.del_component(blk.tmp_init_constraint)

        blk.isentropic.activate()

        # ---------------------------------------------------------------------
        # 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)))

        if activate_performance_curves:
            blk.performance_curve.activate()
            for t, v in unfix_eff.items():
                if v:
                    blk.efficiency_isentropic[t].unfix()
            for t, v in unfix_ratioP.items():
                if v:
                    blk.ratioP[t].unfix()
            with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
                res = opt.solve(blk, tee=slc.tee)
            init_log.info_high(f"Initialization Step 5 {idaeslog.condition(res)}.")

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

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

        return {"vars": var_dict}

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

        self.costing = Block()
        module.pressure_changer_costing(
            self.costing,
            **kwargs)

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

        if hasattr(self, "work_fluid"):
            for t, v in self.work_fluid.items():
                iscale.set_scaling_factor(
                    v,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "work_mechanical"):
            for t, v in self.work_mechanical.items():
                iscale.set_scaling_factor(
                    v,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "work_isentropic"):
            for t, v in self.work_isentropic.items():
                iscale.set_scaling_factor(
                    v,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "ratioP_calculation"):
            for t, c in self.ratioP_calculation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].pressure,
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "fluid_work_calculation"):
            for t, c in self.fluid_work_calculation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.deltaP[t],
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "actual_work"):
            for t, c in self.actual_work.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "isentropic_pressure"):
            for t, c in self.isentropic_pressure.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].pressure,
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "isentropic"):
            for t, c in self.isentropic.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.properties_in[t].entr_mol,
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "isentropic_energy_balance"):
            for t, c in self.isentropic_energy_balance.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True),
                    overwrite=False)

        if hasattr(self, "zero_work_equation"):
            for t, c in self.zero_work_equation.items():
                iscale.constraint_scaling_transform(
                    c,
                    iscale.get_scaling_factor(
                        self.control_volume.work[t],
                        default=1,
                        warning=True))

        if hasattr(self, "state_material_balances"):
            cvol = self.control_volume
            phase_list = cvol.properties_in.phase_list
            phase_component_set = cvol.properties_in.phase_component_set
            mb_type = cvol._constructed_material_balance_type
            if mb_type == MaterialBalanceType.componentPhase:
                for (t, p, j), c in self.state_material_balances.items():
                    sf = iscale.get_scaling_factor(
                        cvol.properties_in[t].get_material_flow_terms(p, j),
                        default=1,
                        warning=True)
                    iscale.constraint_scaling_transform(c, sf)
            elif mb_type == MaterialBalanceType.componentTotal:
                for (t, j), c in self.state_material_balances.items():
                    sf = iscale.min_scaling_factor(
                        [cvol.properties_in[t].get_material_flow_terms(p, j)
                         for p in phase_list if (p, j) in phase_component_set])
                    iscale.constraint_scaling_transform(c, sf)
            else:
                # There are some other material balance types but they create
                # constraints with different names.
                _log.warning(f"Unknown material balance type {mb_type}")

        if hasattr(self, "costing"):
            # import costing scaling factors
            costing.calculate_scaling_factors(self.costing)
Exemple #12
0
class FlowsheetBlockData(ProcessBlockData):
    """
    The FlowsheetBlockData Class forms the base class for all IDAES process
    flowsheet models. The main purpose of this class is to automate the tasks
    common to all flowsheet models and ensure that the necessary attributes of
    a flowsheet model are present.

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

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

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

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

        Args:
            None

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

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

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

        Args:
            None

        Returns:
            True
        """
        return True

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

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

        Args:
            None

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

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

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

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

        # Check for case when dynamic=True, but parent dynamic=False
        elif self.config.dynamic is True:
            if fs is not None and fs.config.dynamic is False:
                raise DynamicError(
                        '{} trying to declare a dynamic model within '
                        'a steady-state flowsheet. This is not '
                        'supported by the IDAES framework. Try '
                        'creating a dynamic flowsheet instead, and '
                        'declaring some models as steady-state.'
                        .format(self.name))

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

                # Set time config argument as reference to time domain
                self.config.time = self.time
            else:
                # Set time config argument to parent time
                self.config.time = fs.config.time
Exemple #13
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=list_of_strings,
            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=In([True, False]),
            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=In([True, False]),
            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().config.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().config.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().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 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 = pp.get_phase_component_set()

        if mb_type == MaterialBalanceType.componentPhase:
            # Create equilibrium generation term and constraints if required
            if self.config.has_phase_equilibrium is True:
                # Get units from property package
                units = {}
                for u in ["holdup", "time"]:
                    try:
                        units[u] = pp.get_metadata().default_units[u]
                    except KeyError:
                        units[u] = "-"

                try:
                    self.phase_equilibrium_generation = Var(
                        self.flowsheet().config.time,
                        pp.phase_equilibrium_idx,
                        domain=Reals,
                        doc="Amount of generation in unit by phase "
                        "equilibria [{}/{}]".format(units["holdup"],
                                                    units["time"]),
                    )
                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().config.time,
                pc_set,
                doc="Material mixing equations",
            )
            def material_mixing_equations(b, t, p, j):
                if (p, j) in pc_set:
                    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))
                else:
                    return Constraint.Skip

        elif mb_type == MaterialBalanceType.componentTotal:
            # Write phase-component balances
            @self.Constraint(
                self.flowsheet().config.time,
                self.config.property_package.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 b.config.property_package.phase_list
                    if (p, j) in pc_set)

        elif mb_type == MaterialBalanceType.total:
            # Write phase-component balances
            @self.Constraint(self.flowsheet().config.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 b.config.property_package.component_list
                        if (p, j) in pc_set)
                    for p in b.config.property_package.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().config.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 b.config.property_package.phase_list)
                for i in range(len(inlet_blocks))) -
                         sum(mixed_block[t].get_enthalpy_flow_terms(p)
                             for p in b.config.property_package.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))
        # Add variables
        self.minimum_pressure = Var(
            self.flowsheet().config.time,
            self.inlet_idx,
            doc="Variable for calculating "
            "minimum inlet pressure",
        )

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

        # Calculate minimum inlet pressure
        @self.Constraint(
            self.flowsheet().config.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().config.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().config.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().config.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=6, optarg={}, solver="ipopt", hold_state=False):
        """
        Initialization routine for mixer (default solver ipopt)

        Keyword Arguments:
            outlvl : sets output level of initialization routine
            optarg : solver options dictionary object (default={})
            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.
        """
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        # Set solver options
        opt = SolverFactory(solver)
        opt.options = 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().config.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().config.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().config.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().config.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 + 1)

    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 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)
            elif mb_type == MaterialBalanceType.componentTotal:
                for (t, j), c in self.material_mixing_equations.items():
                    for i, p in enumerate(
                            self.config.property_package.phase_list):
                        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)
            elif mb_type == MaterialBalanceType.total:
                pc_set = self.config.property_package.get_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)
Exemple #14
0
class ProductData(UnitModelBlockData):
    """
    Standard Product Block 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. 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 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.}""",
        ),
    )

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

        Args:
            None

        Returns:
            None

        """
        # Call UnitModel.build to setup dynamics
        super(ProductData, self).build()

        # Add State Block
        self.properties = self.config.property_package.build_state_block(
            self.flowsheet().config.time,
            doc="Material properties in product",
            default={
                "defined_state": True,
                "has_phase_equilibrium": False,
                **self.config.property_package_args,
            },
        )

        # Add references to all state vars
        s_vars = self.properties[
            self.flowsheet().config.time.first()].define_state_vars()
        for s in s_vars:
            l_name = s_vars[s].local_name
            if s_vars[s].is_indexed():
                slicer = self.properties[:].component(l_name)[...]
            else:
                slicer = self.properties[:].component(l_name)

            r = Reference(slicer)
            setattr(self, s, r)

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

    def initialize(blk,
                   state_args={},
                   outlvl=idaeslog.NOTSET,
                   solver="ipopt",
                   optarg={"tol": 1e-6}):
        """
        This method calls the initialization method of the state block.

        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={'tol': 1e-6})
            solver : str indicating which solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        """
        # ---------------------------------------------------------------------
        # Initialize state block
        init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
        blk.properties.initialize(outlvl=outlvl,
                                  optarg=optarg,
                                  solver=solver,
                                  **state_args)
        init_log.info("Initialization Complete.")

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe({"Inlet": self.inlet},
                                             time_point=time_point)
Exemple #15
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={}, solver=None):
        """
        Initialization routine for splitter

        Keyword Arguments:
            outlvl: sets output level of initialization routine
            optarg: solver options dictionary object (default={})
            solver: str indicating whcih 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)
        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)
        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)
Exemple #16
0
class HelmMixerData(UnitModelBlockData):
    """
    This is a Helmholtz EOS specific mixed 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. 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=list_of_strings,
            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(
        "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.}""",
        ),
    )

    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().build()

        self._get_property_package()

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

        # Build StateBlocks
        self.add_inlet_state_blocks()
        self.add_mixed_state_block()

        @self.Constraint(self.flowsheet().config.time)
        def mass_balance(b, t):
            return self.mixed_state[t].flow_mol == sum(
                self.inlet_blocks[i][t].flow_mol for i in self.inlet_list)

        @self.Constraint(self.flowsheet().config.time)
        def energy_balance(b, t):
            return self.mixed_state[t].enth_mol*self.mixed_state[t].flow_mol == \
                sum(self.inlet_blocks[i][t].enth_mol
                    * self.inlet_blocks[i][t].flow_mol
                    for i in self.inlet_list
                )

        mmx_type = self.config.momentum_mixing_type
        if mmx_type == MomentumMixingType.minimize:
            self.add_pressure_minimization_equations()
        elif mmx_type == MomentumMixingType.equality:
            self.add_pressure_equality_equations()
        elif mmx_type == MomentumMixingType.minimize_and_equality:
            self.add_pressure_minimization_equations()
            self.add_pressure_equality_equations()
            self.use_minimum_inlet_pressure_constraint()

        self.add_port_objects()

    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_{}".format(n)
                for n in range(1, self.config.num_inlets + 1)
            ]
        self.inlet_list = inlet_list

    def add_inlet_state_blocks(self):
        """
        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["defined_state"] = True

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

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

    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["defined_state"] = False

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

    def add_pressure_minimization_equations(self):
        """
        Add pressure minimization equations. This is done by sequential
        comparisons of each inlet to the minimum pressure so far, using
        the IDAES smooth minimum function.
        """
        units_meta = self.config.property_package.get_metadata()
        self.eps_pressure = Param(
            mutable=True,
            initialize=1e-3,
            domain=PositiveReals,
            doc="Smoothing term for minimum inlet pressure",
            units=units_meta.get_derived_units("pressure"))

        # Calculate minimum inlet pressure
        @self.Expression(
            self.flowsheet().config.time,
            self.inlet_list,
            doc="Calculation for minimum inlet pressure",
        )
        def minimum_pressure(b, t, i):
            if i == self.inlet_list[0]:
                return self.inlet_blocks[i][t].pressure
            else:
                pi = self.inlet_list[self.inlet_list.index(i) - 1]
                prev_p = self.minimum_pressure[t, pi]
                this_p = self.inlet_blocks[i][t].pressure
                return smooth_min(this_p, prev_p, self.eps_pressure)

        # Set inlet pressure to minimum pressure
        @self.Constraint(self.flowsheet().config.time,
                         doc="Link pressure to control volume")
        def minimum_pressure_constraint(b, t):
            return self.mixed_state[t].pressure == (
                self.minimum_pressure[t, self.inlet_list[-1]])

    def add_pressure_equality_equations(self):
        """
        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.
        """
        # Create equality constraints
        @self.Constraint(
            self.flowsheet().config.time,
            self.inlet_list,
            doc="Calculation for minimum inlet pressure",
        )
        def pressure_equality_constraints(b, t, i):
            return self.mixed_state[t].pressure == self.inlet_blocks[i][
                t].pressure

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

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

        Returns:
            None
        """
        for p in self.inlet_list:
            self.add_port(name=p, block=self.inlet_blocks[p], doc="Inlet Port")
        self.add_port(name="outlet", block=self.mixed_state, doc="Outlet Port")

    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(self, outlvl=idaeslog.NOTSET, optarg={}, solver=None):
        """
        Initialization routine for mixer.

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

        Returns:
            None
        """
        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)

        # This shouldn't require too much initializtion, just fixing inlets
        # and solving should always work.

        # 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)

        for b in self.inlet_blocks.values():
            for bdat in b.values():
                bdat.pressure.fix()
                bdat.enth_mol.fix()
                bdat.flow_mol.fix()

        for t, v in self.outlet.pressure.items():
            if not v.fixed:
                v.value = min([
                    value(self.inlet_blocks[i][t].pressure)
                    for i in self.inlet_blocks
                ])
        self.outlet.unfix()

        if (hasattr(self, "pressure_equality_constraints")
                and self.pressure_equality_constraints.active):
            # If using the equal pressure constraint fix the outlet and free
            # the inlet pressures, this is typical for pressure driven flow
            for i, b in self.inlet_blocks.items():
                for bdat in b.values():
                    bdat.pressure.unfix()
            self.outlet.pressure.fix()

        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, c in self.mass_balance.items():
            s = iscale.get_scaling_factor(self.mixed_state[t].flow_mol)
            iscale.constraint_scaling_transform(c, s)
        for t, c in self.energy_balance.items():
            s = iscale.get_scaling_factor(self.mixed_state[t].enth_mol)
            s *= iscale.get_scaling_factor(self.mixed_state[t].flow_mol)
            iscale.constraint_scaling_transform(c, s)
        if hasattr(self, "minimum_pressure_constraint"):
            for t, c in self.minimum_pressure_constraint.items():
                s = iscale.get_scaling_factor(self.mixed_state[t].pressure)
                iscale.constraint_scaling_transform(c, s)
        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)
                iscale.constraint_scaling_transform(c, s)
Exemple #17
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.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_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)

    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}
Exemple #18
0
class DowncomerData(UnitModelBlockData):
    """
    Downcomer Unit Class
    """
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "dynamic",
        ConfigValue(domain=DefaultBool,
                    default=useDefault,
                    description="Dynamic model flag",
                    doc="""Indicates whether this model will be dynamic or not,
**default** = useDefault.
**Valid values:** {
**useDefault** - get flag from parent (default = False),
**True** - set as a dynamic model,
**False** - set as a steady-state model.}"""))
    CONFIG.declare(
        "has_holdup",
        ConfigValue(
            default=False,
            domain=Bool,
            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(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of material balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare(
        "has_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(
        "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.}"""))

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


        Args:
            None

        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super().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
            })

        self.control_volume.add_geometry()
        # no phase transitions in the unit - handeled by Helmholtz EoS
        self.control_volume.add_state_blocks(has_phase_equilibrium=False)

        self.control_volume.add_material_balances(
            balance_type=self.config.material_balance_type)

        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=True)

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

        # Add object references
        self.volume = Reference(self.control_volume.volume)

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

        self.deltaP = Reference(self.control_volume.deltaP)

        # Set Unit Geometry and Volume
        self._set_geometry()

        # Construct performance equations
        self._make_performance()

    def _set_geometry(self):
        """
        Define the geometry of the unit as necessary
        """
        units_meta = self.config.property_package.get_metadata()

        # Number of downcomers
        self.number_downcomers = Var(initialize=4,
                                     doc="Number of downcomers for the boiler")
        # Height of downcomer
        self.height = Var(initialize=10.0,
                          doc="Height of downcomer",
                          units=units_meta.get_derived_units("length"))
        # Inside diameter of downcomer
        self.diameter = Var(initialize=0.6,
                            doc="Inside diameter of downcomer",
                            units=units_meta.get_derived_units("length"))
        # Volume constraint
        @self.Constraint(self.flowsheet().time,
                         doc="Downcomer volume of all pipes")
        def volume_eqn(b, t):
            return b.volume[t] == 0.25*const.pi*b.diameter**2*b.height \
                * b.number_downcomers

    def _make_performance(self):
        """
        Define constraints which describe the behaviour of the unit model.
        """
        units_meta = self.config.property_package.get_metadata()

        # Add performance variables
        # Velocity of fluid inside downcomer pipe
        self.velocity = Var(self.flowsheet().time,
                            initialize=10.0,
                            doc='Liquid water velocity inside downcomer',
                            units=units_meta.get_derived_units("velocity"))

        # Reynolds number
        self.N_Re = Var(self.flowsheet().time,
                        initialize=10000.0,
                        doc='Reynolds number')

        # Darcy friction factor (turbulent flow)
        self.friction_factor_darcy = Var(self.flowsheet().time,
                                         initialize=0.005,
                                         doc='Darcy friction factor')

        # Pressure change due to friction
        self.deltaP_friction = Var(
            self.flowsheet().time,
            initialize=-1.0,
            doc='Pressure change due to friction',
            units=units_meta.get_derived_units("pressure"))

        # Pressure change due to gravity
        self.deltaP_gravity = Var(
            self.flowsheet().time,
            initialize=100.0,
            doc='Pressure change due to gravity',
            units=units_meta.get_derived_units("pressure"))

        # Equation for calculating velocity
        @self.Constraint(self.flowsheet().time,
                         doc="Velocity of fluid inside downcomer")
        def velocity_eqn(b, t):
            return b.velocity[t]*0.25*const.pi*b.diameter**2 \
                * b.number_downcomers \
                == b.control_volume.properties_in[t].flow_vol

        # Equation for calculating Reynolds number
        @self.Constraint(self.flowsheet().time, doc="Reynolds number")
        def Reynolds_number_eqn(b, t):
            return b.N_Re[t] * \
                   b.control_volume.properties_in[t].visc_d_phase["Liq"] == \
                   b.diameter * b.velocity[t] *\
                   b.control_volume.properties_in[t].dens_mass_phase["Liq"]

        # Friction factor expression depending on laminar or turbulent flow
        @self.Constraint(self.flowsheet().time,
                         doc="Darcy friction factor as "
                         "a function of Reynolds number")
        def friction_factor_darcy_eqn(b, t):
            return b.friction_factor_darcy[t] * b.N_Re[t]**(0.25) == 0.3164

        # Pressure change equation for friction,
        # -1/2*density*velocity^2*fD/diameter*height
        @self.Constraint(self.flowsheet().time,
                         doc="Pressure change due to friction")
        def pressure_change_friction_eqn(b, t):
            return b.deltaP_friction[t] * b.diameter == -0.5 \
                * b.control_volume.properties_in[t].dens_mass_phase["Liq"] * \
                b.velocity[t]**2 * b.friction_factor_darcy[t] * b.height

        # Pressure change equation for gravity, density*gravity*height
        g_units = units_meta.get_derived_units("acceleration")

        @self.Constraint(self.flowsheet().time,
                         doc="Pressure change due to gravity")
        def pressure_change_gravity_eqn(b, t):
            return b.deltaP_gravity[t] == \
                b.control_volume.properties_in[t].dens_mass_phase["Liq"] \
                * pyunits.convert(const.acceleration_gravity,
                                  to_units=g_units) * b.height

        # Total pressure change equation
        @self.Constraint(self.flowsheet().time, doc="Pressure drop")
        def pressure_change_total_eqn(b, t):
            return b.deltaP[t] == (b.deltaP_friction[t] + b.deltaP_gravity[t])

    def set_initial_condition(self):
        if self.config.dynamic is True:
            self.control_volume.material_accumulation[:, :, :].value = 0
            self.control_volume.energy_accumulation[:, :].value = 0
            self.control_volume.material_accumulation[0, :, :].fix(0)
            self.control_volume.energy_accumulation[0, :].fix(0)

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

        Keyword Arguments:
            state_args : 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")
        solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")

        # Create solver
        opt = get_solver(solver, optarg)

        init_log.info_low("Starting initialization...")

        flags = blk.control_volume.initialize(
            outlvl=outlvl + 1,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )
        init_log.info_high("Initialization Step 1 Complete.")
        # make sure 0 DoF
        if degrees_of_freedom(blk) != 0:
            raise ConfigurationError(
                "Incorrect degrees of freedom when initializing {}: dof = {}".
                format(blk.name, degrees_of_freedom(blk)))
        # Fix outlet pressure
        for t in blk.flowsheet().time:
            blk.control_volume.properties_out[t].pressure.fix(
                value(blk.control_volume.properties_in[t].pressure))
        blk.pressure_change_total_eqn.deactivate()

        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)))

        # Unfix outlet enthalpy and pressure
        for t in blk.flowsheet().time:
            blk.control_volume.properties_out[t].pressure.unfix()
        blk.pressure_change_total_eqn.activate()

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("Initialization Step 3 {}.".format(
            idaeslog.condition(res)))
        blk.control_volume.release_state(flags, outlvl + 1)
        init_log.info("Initialization Complete.")

    def calculate_scaling_factors(self):
        # set a default Reynolds number scaling
        for v in self.N_Re.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-4)

        for v in self.friction_factor_darcy.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 100)

        for v in self.deltaP_gravity.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-3)

        for v in self.deltaP_friction.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-3)

        for t, c in self.volume_eqn.items():
            sf = iscale.get_scaling_factor(self.volume[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf, overwrite=False)

        for t, c in self.Reynolds_number_eqn.items():
            sf = iscale.get_scaling_factor(self.N_Re[t],
                                           default=1,
                                           warning=True)
            sf *= iscale.get_scaling_factor(
                self.control_volume.properties_in[t].visc_d_phase["Liq"],
                default=1,
                warning=True)
            iscale.constraint_scaling_transform(c, sf, overwrite=False)

        for t, c in self.pressure_change_friction_eqn.items():
            sf = iscale.get_scaling_factor(self.deltaP_friction[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf, overwrite=False)

        for t, c in self.pressure_change_gravity_eqn.items():
            sf = iscale.get_scaling_factor(self.deltaP_gravity[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf, overwrite=False)

        for t, c in self.pressure_change_total_eqn.items():
            sf = iscale.get_scaling_factor(self.deltaP[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf, overwrite=False)
Exemple #19
0
class PIDBlockData(ProcessBlockData):
    CONFIG = ProcessBlockData.CONFIG()
    CONFIG.declare(
        "pv",
        ConfigValue(
            default=None,
            description="Measured process variable",
            doc="A Pyomo Var, Expression, or Reference for the measured"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "output",
        ConfigValue(
            default=None,
            description="Controlled process variable",
            doc="A Pyomo Var, Expression, or Reference for the controlled"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "upper",
        ConfigValue(
            default=1.0,
            domain=float,
            description="Output upper limit",
            doc="The upper limit for the controller output, default=1"))
    CONFIG.declare(
        "lower",
        ConfigValue(
            default=0.0,
            domain=float,
            description="Output lower limit",
            doc="The lower limit for the controller output, default=0"))
    CONFIG.declare(
        "calculate_initial_integral",
        ConfigValue(
            default=True,
            domain=bool,
            description="Calculate the initial integral term value if true, "
            " otherwise provide a variable err_i0, which can be fixed",
            doc="Calculate the initial integral term value if true, otherwise"
            " provide a variable err_i0, which can be fixed, default=True"))
    CONFIG.declare(
        "pid_form",
        ConfigValue(default=PIDForm.velocity,
                    domain=In(PIDForm),
                    description="Velocity or standard form",
                    doc="Velocity or standard form"))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        @self.Constraint(time_set, doc="Controller output constraint")
        def output_constraint(b, t):
            if t == t0:
                return pyo.Constraint.Skip
            else:
                return self.output[t] ==\
                    smooth_min(
                        smooth_max(self.unconstrained_output[t], l, e), h, e)
Exemple #20
0
class BatteryStorageData(UnitModelBlockData):
    """
    Wind plant using turbine powercurve and resource data.
    Unit model to convert wind resource into electricity.
    """
    CONFIG = ConfigBlock()
    CONFIG.declare("dynamic", ConfigValue(
        domain=In([False]),
        default=False,
        description="Dynamic model flag - must be False",
        doc="""Battery 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="""Battery does not have defined volume, thus this must be False."""))

    def build(self):
        """Building model
        This model does not use the flowsheet's time domain. Instead, it only models a single timestep, with initial
        conditions provided by `initial_state_of_charge` and `initial_energy_throughput`. The model calculates change
        in stored energy across a single time step using the power flow variables, `power_in` and `power_out`, and
        the `dr_hr` parameter.
        Args:
            None
        Returns:
            None
        """
        super().build()

        # Design variables and parameters
        self.nameplate_power = Var(within=NonNegativeReals,
                                   initialize=0.0,
                                   bounds=(0, 1e6),
                                   doc="Nameplate power of battery energy storage",
                                   units=pyunits.kW)

        self.nameplate_energy = Var(within=NonNegativeReals,
                                    initialize=0.0,
                                    bounds=(0, 1e7),
                                    doc="Nameplate energy of battery energy storage",
                                    units=pyunits.kWh)

        self.charging_eta = Param(within=NonNegativeReals,
                                  mutable=True,
                                  initialize=1,
                                  doc="Charging efficiency, (0, 1]")

        self.discharging_eta = Param(within=NonNegativeReals,
                                     mutable=True,
                                     initialize=1,
                                     doc="Discharging efficiency, (0, 1]")

        self.degradation_rate = Param(within=NonNegativeReals,
                                      mutable=True,
                                      initialize=0.8/3800,
                                      doc="Degradation rate, [0, 2.5e-3]",
                                      units=pyunits.hr/pyunits.hr)

        # Initial conditions
        self.initial_state_of_charge = Var(within=NonNegativeReals,
                                           initialize=0.0,
                                           doc="State of charge at t - 1, [0, self.nameplate_energy]",
                                           units=pyunits.kWh)

        self.initial_energy_throughput = Var(within=NonNegativeReals,
                                             initialize=0.0,
                                             doc="Cumulative energy throughput at t - 1",
                                             units=pyunits.kWh)

        # Power flows and energy storage
        self.dt = Param(within=NonNegativeReals,
                        initialize=1,
                        doc="Time step for converting between electricity power flows and stored energy",
                        units=pyunits.hr)

        self.elec_in = Var(within=NonNegativeReals,
                           initialize=0.0,
                           doc="Energy in",
                           units=pyunits.kW)

        self.elec_out = Var(within=NonNegativeReals,
                            initialize=0.0,
                            doc="Energy out",
                            units=pyunits.kW)

        self.state_of_charge = Var(within=NonNegativeReals,
                                   initialize=0.0,
                                   doc="State of charge (energy), [0, self.nameplate_energy]",
                                   units=pyunits.kWh)

        self.energy_throughput = Var(within=NonNegativeReals,
                                     initialize=0.0,
                                     doc="Cumulative energy throughput",
                                     units=pyunits.kWh)

        # Ports
        self.power_in = Port(noruleinit=True, doc="A port for electricity inflow")
        self.power_in.add(self.elec_in, "electricity")

        self.power_out = Port(noruleinit=True, doc="A port for electricity outflow")
        self.power_out.add(self.elec_out, "electricity")

        @self.Constraint(self.flowsheet().config.time)
        def state_evolution(b):
            return b.state_of_charge == b.initial_state_of_charge + (
                    b.charging_eta * b.dt * b.elec_in
                    - b.dt / b.discharging_eta * b.elec_out)

        @self.Constraint(self.flowsheet().config.time)
        def accumulate_energy_throughput(b):
            return b.energy_throughput == b.initial_energy_throughput + b.dt * (b.elec_in + b.elec_out) / 2

        @self.Constraint(self.flowsheet().config.time)
        def state_of_charge_bounds(b):
            return b.state_of_charge <= b.nameplate_energy - b.degradation_rate * b.energy_throughput

        @self.Constraint(self.flowsheet().config.time)
        def power_bound_in(b):
            return b.elec_in <= b.nameplate_power

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

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

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

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

        Args:
            attr (str): Attribute name to check

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

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

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

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

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

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

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

        Args:
            None

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    def __getattr__(self, attr):
        """
        This method is used to avoid generating unnecessary property
        calculations in state blocks. __getattr__ is called whenever a
        property is called for, and if a propery does not exist, it looks for
        a method to create the required property, and any associated
        components.

        Create a property calculation if needed. Return an attrbute error if
        attr == 'domain' or starts with a _ . The error for _ prevents a
        recursion error if trying to get a function to create a property and
        that function doesn't exist.  Pyomo also ocasionally looks for things
        that start with _ and may not exist.  Pyomo also looks for the domain
        attribute, and it may not exist.
        This works by creating a property calculation by calling the "_"+attr
        function.

        A list of __getattr__ calls is maintained in self.__getattrcalls to
        check for recursive loops which maybe useful for debugging. This list
        is cleared after __getattr__ completes successfully.

        Args:
            attr: an attribute to create and return. Should be a property
                  component.
        """
        if self._lock_attribute_creation:
            raise AttributeError(
                f"{attr} does not exist, and attribute creation is locked.")

        def clear_call_list(self, attr):
            """Local method for cleaning up call list when a call is handled.

                Args:
                    attr: attribute currently being handled
            """
            if self.__getattrcalls[-1] == attr:
                if len(self.__getattrcalls) <= 1:
                    del self.__getattrcalls
                else:
                    del self.__getattrcalls[-1]
            else:
                raise PropertyPackageError(
                    "{} Trying to remove call {} from __getattr__"
                    " call list, however this is not the most "
                    "recent call in the list ({}). This indicates"
                    " a bug in the __getattr__ calls. Please "
                    "contact the IDAES developers with this bug.".format(
                        self.name, attr, self.__getattrcalls[-1]))

        # Check that attr is not something we shouldn't touch
        if attr == "domain" or attr.startswith("_"):
            # Don't interfere with anything by getting attributes that are
            # none of my business
            raise PropertyPackageError(
                '{} {} does not exist, but is a protected '
                'attribute. Check the naming of your '
                'components to avoid any reserved names'.format(
                    self.name, attr))

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

        # Check for recursive calls
        try:
            # Check if __getattrcalls is initialized
            self.__getattrcalls
        except AttributeError:
            # Initialize it
            self.__getattrcalls = [attr]
        else:
            # Check to see if attr already appears in call list
            if attr in self.__getattrcalls:
                # If it does, indicates a recursive loop.
                if attr == self.__getattrcalls[-1]:
                    # attr method is calling itself
                    self.__getattrcalls.append(attr)
                    raise PropertyPackageError(
                        '{} _{} made a recursive call to '
                        'itself, indicating a potential '
                        'recursive loop. This is generally '
                        'caused by the {} method failing to '
                        'create the {} component.'.format(
                            self.name, attr, attr, attr))
                else:
                    self.__getattrcalls.append(attr)
                    raise PropertyPackageError(
                        '{} a potential recursive loop has been '
                        'detected whilst trying to construct {}. '
                        'A method was called, but resulted in a '
                        'subsequent call to itself, indicating a '
                        'recursive loop. This may be caused by a '
                        'method trying to access a component out '
                        'of order for some reason (e.g. it is '
                        'declared later in the same method). See '
                        'the __getattrcalls object for a list of '
                        'components called in the __getattr__ '
                        'sequence.'.format(self.name, attr))
            # If not, add call to list
            self.__getattrcalls.append(attr)

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

            if m is None:
                raise PropertyPackageError(
                    '{} property package get_metadata()'
                    ' method returned None when trying to create '
                    '{}. Please contact the developer of the '
                    'property package'.format(self.name, attr))
        except KeyError:
            # If attr not in metadata, assume package does not
            # support property
            clear_call_list(self, attr)
            raise PropertyNotSupportedError(
                '{} {} is not supported by property package (property is '
                'not listed in package metadata properties).'.format(
                    self.name, attr))

        # Get method name from resulting properties
        try:
            if m[attr]['method'] is None:
                # If method is none, property should be constructed
                # by property package, so raise PropertyPackageError
                clear_call_list(self, attr)
                raise PropertyPackageError(
                    '{} {} should be constructed automatically '
                    'by property package, but is not present. '
                    'This can be caused by methods being called '
                    'out of order.'.format(self.name, attr))
            elif m[attr]['method'] is False:
                # If method is False, package does not support property
                # Raise NotImplementedError
                clear_call_list(self, attr)
                raise PropertyNotSupportedError(
                    '{} {} is not supported by property package '
                    '(property method is listed as False in '
                    'package property metadata).'.format(self.name, attr))
            elif isinstance(m[attr]['method'], str):
                # Try to get method name in from PropertyBlock object
                try:
                    f = getattr(self, m[attr]['method'])
                except AttributeError:
                    # If fails, method does not exist
                    clear_call_list(self, attr)
                    raise PropertyPackageError(
                        '{} {} package property metadata method '
                        'returned a name that does not correspond'
                        ' to any method in the property package. '
                        'Please contact the developer of the '
                        'property package.'.format(self.name, attr))
            else:
                # Otherwise method name is invalid
                clear_call_list(self, attr)
                raise PropertyPackageError(
                    '{} {} package property metadata method '
                    'returned invalid value for method name. '
                    'Please contact the developer of the '
                    'property package.'.format(self.name, attr))
        except KeyError:
            # No method key - raise Exception
            # Need to use an AttributeError so Pyomo.DAE will handle this
            clear_call_list(self, attr)
            raise PropertyNotSupportedError(
                '{} package property metadata method '
                'does not contain a method for {}. '
                'Please select a package which supports '
                'the necessary properties for your process.'.format(
                    self.name, attr))

        # Call attribute if it is callable
        # If this fails, it should return a meaningful error.
        if callable(f):
            try:
                f()
            except Exception:
                # Clear call list and reraise error
                clear_call_list(self, attr)
                raise
        else:
            # If f is not callable, inform the user and clear call list
            clear_call_list(self, attr)
            raise PropertyPackageError(
                '{} tried calling attribute {} in order to create '
                'component {}. However the method is not callable.'.format(
                    self.name, f, attr))

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

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        # Get scaling factor defaults, if no scaling factor set
        for v in self.component_data_objects((Constraint, Var, Expression),
                                             descend_into=False):
            if iscale.get_scaling_factor(v) is None:  # don't replace if set
                name = v.getname().split("[")[0]
                index = v.index()
                sf = self.config.parameters.get_default_scaling(name, index)
                if sf is not None:
                    iscale.set_scaling_factor(v, sf)
Exemple #22
0
class ReactionBlockData(ReactionBlockDataBase):
    """
    Heterogeneous reaction package for methane reacting with Fe2O3 based OC
    """
    # Create Class ConfigBlock
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "parameters",
        ConfigValue(domain=is_reaction_parameter_block,
                    description="""
            A reference to an instance of the Reaction Parameter
            Block associated with this property package.
            """))
    CONFIG.declare(
        "solid_state_block",
        ConfigValue(domain=is_state_block,
                    description="""
            A reference to an instance of a StateBlock for the
            solid phase with which this reaction block should be associated.
            """))
    CONFIG.declare(
        "gas_state_block",
        ConfigValue(domain=is_state_block,
                    description="""
            A reference to an instance of a StateBlock for the
            gas phase with which this reaction block should be associated.
            """))
    CONFIG.declare(
        "has_equilibrium",
        ConfigValue(default=False,
                    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.}
        """))

    def build(self):
        """
        Callable method for Block construction
        """
        super(ReactionBlockDataBase, self).build()

        # Object references to the corresponding state blocks and parameters
        add_object_reference(self, "_params", self.config.parameters)

        add_object_reference(self, "solid_state_ref",
                             self.config.solid_state_block[self.index()])
        add_object_reference(self, "gas_state_ref",
                             self.config.gas_state_block[self.index()])

        # Object reference for parameters if needed by CV1D
        # Reaction stoichiometry
        add_object_reference(
            self, "rate_reaction_stoichiometry",
            self.config.parameters.rate_reaction_stoichiometry)

        # Heat of reaction
        add_object_reference(self, "dh_rxn", self.config.parameters.dh_rxn)

    # Rate constant method
    def _k_rxn(self):
        self.k_rxn = Var(self._params.rate_reaction_idx,
                         domain=Reals,
                         initialize=1,
                         doc='Rate constant '
                         '[mol^(1-N_reaction)m^(3*N_reaction -2)/s]')

        def rate_constant_eqn(b, j):
            if j == 'R1':
                return 1e6 * self.k_rxn[j] == \
                        1e6 * (self._params.k0_rxn[j] *
                               exp(-self._params.energy_activation[j] /
                                   (self._params.gas_const *
                                    self.solid_state_ref.temperature)))
            else:
                return Constraint.Skip

        try:
            # Try to build constraint
            self.rate_constant_eqn = Constraint(self._params.rate_reaction_idx,
                                                rule=rate_constant_eqn)
        except AttributeError:
            # If constraint fails, clean up so that DAE can try again later
            self.del_component(self.k_rxn)
            self.del_component(self.rate_constant_eqn)
            raise

    # Conversion of oxygen carrier
    def _OC_conv(self):
        self.OC_conv = Var(domain=Reals,
                           initialize=0.0,
                           doc='Fraction of metal oxide converted')

        def OC_conv_eqn(b):
            return 1e6 * b.OC_conv * \
                   (b.solid_state_ref.mass_frac_comp['Fe3O4'] +
                    (b.solid_state_ref._params.mw_comp['Fe3O4'] /
                        b.solid_state_ref._params.mw_comp['Fe2O3']) *
                    (b._params.rate_reaction_stoichiometry
                       ['R1', 'Sol', 'Fe3O4']
                       / -b._params.rate_reaction_stoichiometry
                       ['R1', 'Sol', 'Fe2O3']) *
                    b.solid_state_ref.mass_frac_comp['Fe2O3']) == \
                   1e6 * b.solid_state_ref.mass_frac_comp['Fe3O4']

        try:
            # Try to build constraint
            self.OC_conv_eqn = Constraint(rule=OC_conv_eqn)
        except AttributeError:
            # If constraint fails, clean up so that DAE can try again later
            self.del_component(self.OC_conv)
            self.del_component(self.OC_conv_eqn)

    # Conversion of oxygen carrier reformulated
    def _OC_conv_temp(self):
        self.OC_conv_temp = Var(domain=Reals,
                                initialize=1.0,
                                doc='Reformulation term for'
                                'X to help eqn scaling')

        def OC_conv_temp_eqn(b):
            return 1e3 * b.OC_conv_temp**3 == 1e3 * (1 - b.OC_conv)**2

        try:
            # Try to build constraint
            self.OC_conv_temp_eqn = Constraint(rule=OC_conv_temp_eqn)
        except AttributeError:
            # If constraint fails, clean up so that DAE can try again later
            self.del_component(self.OC_conv_temp)
            self.del_component(self.OC_conv_temp_eqn)

    # General rate of reaction method
    def _reaction_rate(self):
        self.reaction_rate = Var(self._params.rate_reaction_idx,
                                 domain=Reals,
                                 initialize=0,
                                 doc="Gen. rate of reaction [mol_rxn/m3.s]")

        def rate_rule(b, r):
            return b.reaction_rate[
                r] * 1e4 == b._params._scale_factor_rxn * 1e4 * (
                    b.solid_state_ref.mass_frac_comp['Fe2O3'] *
                    (1 - b.solid_state_ref.particle_porosity) *
                    b.solid_state_ref.dens_mass_skeletal *
                    (b._params.a_vol /
                     (b.solid_state_ref._params.mw_comp['Fe2O3'])) * 3 *
                    b._params.rxn_stoich_coeff[r] * b.k_rxn[r] *
                    (((b.gas_state_ref.dens_mol_comp['CH4']**2 + b._params.eps
                       **2)**0.5)**b._params.rxn_order[r]) * b.OC_conv_temp /
                    (b._params.dens_mol_sol * b._params.grain_radius) /
                    (-b._params.rate_reaction_stoichiometry['R1', 'Sol',
                                                            'Fe2O3']))

        try:
            # Try to build constraint
            self.gen_rate_expression = Constraint(
                self._params.rate_reaction_idx, rule=rate_rule)
        except AttributeError:
            # If constraint fails, clean up so that DAE can try again later
            self.del_component(self.reaction_rate)
            self.del_component(self.gen_rate_expression)
            raise

    def get_reaction_rate_basis(b):
        return MaterialFlowBasis.molar

    def model_check(blk):
        """
        Model checks for property block
        """
        # Check temperature bounds
        if value(blk.temperature) < blk.temperature.lb:
            _log.error('{} Temperature set below lower bound.'.format(
                blk.name))
        if value(blk.temperature) > blk.temperature.ub:
            _log.error('{} Temperature set above upper bound.'.format(
                blk.name))
Exemple #23
0
class GDPbbSolver(object):
    """
    A branch and bound-based solver for Generalized Disjunctive Programming (GDP) problems

    The GDPbb solver solves subproblems relaxing certain disjunctions, and
    builds up a tree of potential active disjunctions. By exploring promising
    branches, it eventually results in an optimal configuration of disjunctions.

    Keyword arguments below are specified for the ``solve`` function.

    """
    CONFIG = ConfigBlock("gdpbb")
    CONFIG.declare(
        "solver",
        ConfigValue(default="baron",
                    description="Subproblem solver to use, defaults to baron"))
    CONFIG.declare(
        "solver_args",
        ConfigBlock(
            implicit=True,
            description="Block of keyword arguments to pass to the solver."))
    CONFIG.declare(
        "tee",
        ConfigValue(default=False,
                    domain=bool,
                    description="Flag to stream solver output to console."))
    CONFIG.declare(
        "check_sat",
        ConfigValue(
            default=False,
            domain=bool,
            description=
            "When True, GDPBB will check satisfiability via the pyomo.contrib.satsolver interface at each node"
        ))
    CONFIG.declare(
        "logger",
        ConfigValue(
            default='pyomo.contrib.gdpbb',
            description="The logger object or name to use for reporting.",
            domain=a_logger))
    CONFIG.declare(
        "time_limit",
        ConfigValue(
            default=600,
            domain=PositiveInt,
            description="Time limit (seconds, default=600)",
            doc="Seconds allowed until terminated. Note that the time limit can"
            "currently only be enforced between subsolver invocations. You may"
            "need to set subsolver time limits as well."))

    @deprecated("GDPbb has been merged into GDPopt. "
                "You can use the algorithm using GDPopt with strategy='LBB'.",
                logger="pyomo.solvers",
                version='TBD',
                remove_in='TBD')
    def __init__(self, *args, **kwargs):
        super(GDPbbSolver, self).__init__(*args, **kwargs)

    def available(self, exception_flag=True):
        """Check if solver is available.

        TODO: For now, it is always available. However, sub-solvers may not
        always be available, and so this should reflect that possibility.

        """
        return True

    def version(self):
        return __version__

    def solve(self, model, **kwds):
        config = self.CONFIG(kwds.pop('options', {}))
        config.set_value(kwds)
        return SolverFactory('gdpopt').solve(
            model,
            strategy='LBB',
            minlp_solver=config.solver,
            minlp_solver_args=config.solver_args,
            tee=config.tee,
            check_sat=config.check_sat,
            logger=config.logger,
            time_limit=config.time_limit)

        # Validate model to be used with gdpbb
        self.validate_model(model)
        # Set solver as an MINLP
        solve_data = GDPbbSolveData()
        solve_data.timing = Container()
        solve_data.original_model = model
        solve_data.results = SolverResults()

        old_logger_level = config.logger.getEffectiveLevel()
        with time_code(solve_data.timing, 'total', is_main_timer=True), \
                restore_logger_level(config.logger), \
                create_utility_block(model, 'GDPbb_utils', solve_data):
            if config.tee and old_logger_level > logging.INFO:
                # If the logger does not already include INFO, include it.
                config.logger.setLevel(logging.INFO)
            config.logger.info(
                "Starting GDPbb version %s using %s as subsolver" %
                (".".join(map(str, self.version())), config.solver))

            # Setup results
            solve_data.results.solver.name = 'GDPbb - %s' % (str(
                config.solver))
            setup_results_object(solve_data, config)

            # clone original model for root node of branch and bound
            root = solve_data.working_model = solve_data.original_model.clone()

            # get objective sense
            process_objective(solve_data, config)
            objectives = solve_data.original_model.component_data_objects(
                Objective, active=True)
            obj = next(objectives, None)
            solve_data.results.problem.sense = obj.sense

            # set up lists to keep track of which disjunctions have been covered.

            # this list keeps track of the relaxed disjunctions
            root.GDPbb_utils.unenforced_disjunctions = list(
                disjunction
                for disjunction in root.GDPbb_utils.disjunction_list
                if disjunction.active)

            root.GDPbb_utils.deactivated_constraints = ComponentSet([
                constr
                for disjunction in root.GDPbb_utils.unenforced_disjunctions
                for disjunct in disjunction.disjuncts
                for constr in disjunct.component_data_objects(ctype=Constraint,
                                                              active=True)
                if constr.body.polynomial_degree() not in (1, 0)
            ])
            # Deactivate nonlinear constraints in unenforced disjunctions
            for constr in root.GDPbb_utils.deactivated_constraints:
                constr.deactivate()

            # Add the BigM suffix if it does not already exist. Used later during nonlinear constraint activation.
            if not hasattr(root, 'BigM'):
                root.BigM = Suffix()

            # Pre-screen that none of the disjunctions are already predetermined due to the disjuncts being fixed
            # to True/False values.
            # TODO this should also be done within the loop, but we aren't handling it right now.
            # Should affect efficiency, but not correctness.
            root.GDPbb_utils.disjuncts_fixed_True = ComponentSet()
            # Only find top-level (non-nested) disjunctions
            for disjunction in root.component_data_objects(Disjunction,
                                                           active=True):
                fixed_true_disjuncts = [
                    disjunct for disjunct in disjunction.disjuncts
                    if disjunct.indicator_var.fixed
                    and disjunct.indicator_var.value == 1
                ]
                fixed_false_disjuncts = [
                    disjunct for disjunct in disjunction.disjuncts
                    if disjunct.indicator_var.fixed
                    and disjunct.indicator_var.value == 0
                ]
                for disjunct in fixed_false_disjuncts:
                    disjunct.deactivate()
                if len(fixed_false_disjuncts) == len(
                        disjunction.disjuncts) - 1:
                    # all but one disjunct in the disjunction is fixed to False. Remaining one must be true.
                    if not fixed_true_disjuncts:
                        fixed_true_disjuncts = [
                            disjunct for disjunct in disjunction.disjuncts
                            if disjunct not in fixed_false_disjuncts
                        ]
                # Reactivate the fixed-true disjuncts
                for disjunct in fixed_true_disjuncts:
                    newly_activated = ComponentSet()
                    for constr in disjunct.component_data_objects(Constraint):
                        if constr in root.GDPbb_utils.deactivated_constraints:
                            newly_activated.add(constr)
                            constr.activate()
                            # Set the big M value for the constraint
                            root.BigM[constr] = 1
                            # Note: we use a default big M value of 1
                            # because all non-selected disjuncts should be deactivated.
                            # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed.
                            # The default M value should therefore be irrelevant.
                    root.GDPbb_utils.deactivated_constraints -= newly_activated
                    root.GDPbb_utils.disjuncts_fixed_True.add(disjunct)

                if fixed_true_disjuncts:
                    assert disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \
                        "%s violates this assumption." % (disjunction.name, )
                    root.GDPbb_utils.unenforced_disjunctions.remove(
                        disjunction)

            # Check satisfiability
            if config.check_sat and satisfiable(root, config.logger) is False:
                # Problem is not satisfiable. Problem is infeasible.
                obj_value = obj_sign * float('inf')
            else:
                # solve the root node
                config.logger.info("Solving the root node.")
                obj_value, result, var_values = self.subproblem_solve(
                    root, config)

            if obj_sign * obj_value == float('inf'):
                config.logger.info(
                    "Model was found to be infeasible at the root node. Elapsed %.2f seconds."
                    % get_main_elapsed_time(solve_data.timing))
                if solve_data.results.problem.sense == minimize:
                    solve_data.results.problem.lower_bound = float('inf')
                    solve_data.results.problem.upper_bound = None
                else:
                    solve_data.results.problem.lower_bound = None
                    solve_data.results.problem.upper_bound = float('-inf')
                solve_data.results.solver.timing = solve_data.timing
                solve_data.results.solver.iterations = 0
                solve_data.results.solver.termination_condition = tc.infeasible
                return solve_data.results

            # initialize minheap for Branch and Bound algorithm
            # Heap structure: (ordering tuple, model)
            # Ordering tuple: (objective value, disjunctions_left, -total_nodes_counter)
            #  - select solutions with lower objective value,
            #    then fewer disjunctions left to explore (depth first),
            #    then more recently encountered (tiebreaker)
            heap = []
            total_nodes_counter = 0
            disjunctions_left = len(root.GDPbb_utils.unenforced_disjunctions)
            heapq.heappush(heap,
                           ((obj_sign * obj_value, disjunctions_left,
                             -total_nodes_counter), root, result, var_values))

            # loop to branch through the tree
            while len(heap) > 0:
                # pop best model off of heap
                sort_tuple, incumbent_model, incumbent_results, incumbent_var_values = heapq.heappop(
                    heap)
                incumbent_obj_value, disjunctions_left, _ = sort_tuple

                config.logger.info(
                    "Exploring node with LB %.10g and %s inactive disjunctions."
                    % (incumbent_obj_value, disjunctions_left))

                # if all the originally active disjunctions are active, solve and
                # return solution
                if disjunctions_left == 0:
                    config.logger.info("Model solved.")
                    # Model is solved. Copy over solution values.
                    original_model = solve_data.original_model
                    for orig_var, val in zip(
                            original_model.GDPbb_utils.variable_list,
                            incumbent_var_values):
                        orig_var.value = val

                    solve_data.results.problem.lower_bound = incumbent_results.problem.lower_bound
                    solve_data.results.problem.upper_bound = incumbent_results.problem.upper_bound
                    solve_data.results.solver.timing = solve_data.timing
                    solve_data.results.solver.iterations = total_nodes_counter
                    solve_data.results.solver.termination_condition = incumbent_results.solver.termination_condition
                    return solve_data.results

                # Pick the next disjunction to branch on
                next_disjunction = incumbent_model.GDPbb_utils.unenforced_disjunctions[
                    0]
                config.logger.info("Branching on disjunction %s" %
                                   next_disjunction.name)
                assert next_disjunction.xor, "GDPbb only handles disjunctions in which one term can be selected. " \
                    "%s violates this assumption." % (next_disjunction.name, )

                new_nodes_counter = 0

                for i, disjunct in enumerate(next_disjunction.disjuncts):
                    # Create one branch for each of the disjuncts on the disjunction

                    if any(disj.indicator_var.fixed
                           and disj.indicator_var.value == 1
                           for disj in next_disjunction.disjuncts
                           if disj is not disjunct):
                        # If any other disjunct is fixed to 1 and an xor relationship applies,
                        # then this disjunct cannot be activated.
                        continue

                    # Check time limit
                    if get_main_elapsed_time(
                            solve_data.timing) >= config.time_limit:
                        if solve_data.results.problem.sense == minimize:
                            solve_data.results.problem.lower_bound = incumbent_obj_value
                            solve_data.results.problem.upper_bound = float(
                                'inf')
                        else:
                            solve_data.results.problem.lower_bound = float(
                                '-inf')
                            solve_data.results.problem.upper_bound = incumbent_obj_value
                        config.logger.info('GDPopt unable to converge bounds '
                                           'before time limit of {} seconds. '
                                           'Elapsed: {} seconds'.format(
                                               config.time_limit,
                                               get_main_elapsed_time(
                                                   solve_data.timing)))
                        config.logger.info(
                            'Final bound values: LB: {}  UB: {}'.format(
                                solve_data.results.problem.lower_bound,
                                solve_data.results.problem.upper_bound))
                        solve_data.results.solver.timing = solve_data.timing
                        solve_data.results.solver.iterations = total_nodes_counter
                        solve_data.results.solver.termination_condition = tc.maxTimeLimit
                        return solve_data.results

                    # Branch on the disjunct
                    child = incumbent_model.clone()
                    # TODO I am leaving the old branching system in place, but there should be
                    # something better, ideally that deals with nested disjunctions as well.
                    disjunction_to_branch = child.GDPbb_utils.unenforced_disjunctions.pop(
                        0)
                    child_disjunct = disjunction_to_branch.disjuncts[i]
                    child_disjunct.indicator_var.fix(1)
                    # Deactivate (and fix to 0) other disjuncts on the disjunction
                    for disj in disjunction_to_branch.disjuncts:
                        if disj is not child_disjunct:
                            disj.deactivate()
                    # Activate nonlinear constraints on the newly fixed child disjunct
                    newly_activated = ComponentSet()
                    for constr in child_disjunct.component_data_objects(
                            Constraint):
                        if constr in child.GDPbb_utils.deactivated_constraints:
                            newly_activated.add(constr)
                            constr.activate()
                            # Set the big M value for the constraint
                            child.BigM[constr] = 1
                            # Note: we use a default big M value of 1
                            # because all non-selected disjuncts should be deactivated.
                            # Therefore, none of the big M transformed nonlinear constraints will need to be relaxed.
                            # The default M value should therefore be irrelevant.
                    child.GDPbb_utils.deactivated_constraints -= newly_activated
                    child.GDPbb_utils.disjuncts_fixed_True.add(child_disjunct)

                    if disjunct in incumbent_model.GDPbb_utils.disjuncts_fixed_True:
                        # If the disjunct was already branched to True from a parent disjunct branching, just pass
                        # through the incumbent value without resolving. The solution should be the same as the parent.
                        total_nodes_counter += 1
                        ordering_tuple = (obj_sign * incumbent_obj_value,
                                          disjunctions_left - 1,
                                          -total_nodes_counter)
                        heapq.heappush(heap, (ordering_tuple, child, result,
                                              incumbent_var_values))
                        new_nodes_counter += 1
                        continue

                    if config.check_sat and satisfiable(
                            child, config.logger) is False:
                        # Problem is not satisfiable. Skip this disjunct.
                        continue

                    obj_value, result, var_values = self.subproblem_solve(
                        child, config)
                    total_nodes_counter += 1
                    ordering_tuple = (obj_sign * obj_value,
                                      disjunctions_left - 1,
                                      -total_nodes_counter)
                    heapq.heappush(heap,
                                   (ordering_tuple, child, result, var_values))
                    new_nodes_counter += 1

                config.logger.info(
                    "Added %s new nodes with %s relaxed disjunctions to the heap. Size now %s."
                    % (new_nodes_counter, disjunctions_left - 1, len(heap)))

    @staticmethod
    def validate_model(model):
        # Validates that model has only exclusive disjunctions
        for d in model.component_data_objects(ctype=Disjunction, active=True):
            if not d.xor:
                raise ValueError('GDPbb solver unable to handle '
                                 'non-exclusive disjunctions')
        objectives = model.component_data_objects(Objective, active=True)
        obj = next(objectives, None)
        if next(objectives, None) is not None:
            raise RuntimeError(
                "GDPbb solver is unable to handle model with multiple active objectives."
            )
        if obj is None:
            raise RuntimeError(
                "GDPbb solver is unable to handle model with no active objective."
            )

    @staticmethod
    def subproblem_solve(gdp, config):
        subproblem = gdp.clone()
        TransformationFactory('gdp.bigm').apply_to(subproblem)
        main_obj = next(
            subproblem.component_data_objects(Objective, active=True))
        obj_sign = 1 if main_obj.sense == minimize else -1

        try:
            result = SolverFactory(config.solver).solve(
                subproblem, **config.solver_args)
        except RuntimeError as e:
            config.logger.warning(
                "Solver encountered RuntimeError. Treating as infeasible. "
                "Msg: %s\n%s" % (str(e), traceback.format_exc()))
            var_values = [
                v.value for v in subproblem.GDPbb_utils.variable_list
            ]
            return obj_sign * float('inf'), SolverResults(), var_values

        var_values = [v.value for v in subproblem.GDPbb_utils.variable_list]
        term_cond = result.solver.termination_condition
        if result.solver.status is SolverStatus.ok and any(
                term_cond == valid_cond
                for valid_cond in (tc.optimal, tc.locallyOptimal,
                                   tc.feasible)):
            return value(main_obj.expr), result, var_values
        elif term_cond == tc.unbounded:
            return obj_sign * float('-inf'), result, var_values
        elif term_cond == tc.infeasible:
            return obj_sign * float('inf'), result, var_values
        else:
            config.logger.warning("Unknown termination condition of %s" %
                                  term_cond)
            return obj_sign * float('inf'), result, var_values

    def __enter__(self):
        return self

    def __exit__(self, t, v, traceback):
        pass
Exemple #24
0
class ReactionParameterData(ReactionParameterBlock):
    """
    Property Parameter Block Class

    Contains parameters and indexing sets associated with properties for
    superheated steam.

    """
    # Create Class ConfigBlock
    CONFIG = ConfigBlock()
    CONFIG.declare(
        "gas_property_package",
        ConfigValue(
            description="Reference to associated PropertyPackageParameter "
            "object for the gas phase.",
            domain=is_physical_parameter_block))
    CONFIG.declare(
        "solid_property_package",
        ConfigValue(
            description="Reference to associated PropertyPackageParameter "
            "object for the solid phase.",
            domain=is_physical_parameter_block))
    CONFIG.declare(
        "default_arguments",
        ConfigBlock(
            description="Default arguments to use with Property Package",
            implicit=True))

    def build(self):
        '''
        Callable method for Block construction.
        '''
        super(ReactionParameterBlock, self).build()

        self._reaction_block_class = ReactionBlock

        # Create Phase objects
        self.Vap = VaporPhase()
        self.Sol = SolidPhase()

        # Create Component objects
        self.CH4 = Component()
        self.CO2 = Component()
        self.H2O = Component()
        self.Fe2O3 = Component()
        self.Fe3O4 = Component()
        self.Al2O3 = Component()

        # Component list subsets
        self.gas_component_list = Set(initialize=['CO2', 'H2O', 'CH4'])
        self.sol_component_list = Set(initialize=['Fe2O3', 'Fe3O4', 'Al2O3'])

        # Reaction Index
        self.rate_reaction_idx = Set(initialize=["R1"])

        # Gas Constant
        self.gas_const = Param(within=PositiveReals,
                               mutable=False,
                               default=8.314459848e-3,
                               doc='Gas Constant [kJ/mol.K]')

        # Smoothing factor
        self.eps = Param(mutable=True, default=1e-8, doc='Smoothing Factor')
        # Reaction rate scale factor
        self._scale_factor_rxn = Param(mutable=True,
                                       default=1,
                                       doc='Scale Factor for reaction eqn.'
                                       'Used to help initialization routine')

        # Reaction Stoichiometry
        self.rate_reaction_stoichiometry = {
            ("R1", "Vap", "CH4"): -1,
            ("R1", "Vap", "CO2"): 1,
            ("R1", "Vap", "H2O"): 2,
            ("R1", "Sol", "Fe2O3"): -12,
            ("R1", "Sol", "Fe3O4"): 8,
            ("R1", "Sol", "Al2O3"): 0
        }

        # Reaction stoichiometric coefficient
        self.rxn_stoich_coeff = Param(self.rate_reaction_idx,
                                      default=12,
                                      mutable=True,
                                      doc='Reaction stoichiometric'
                                      'coefficient [-]')

        # Standard Heat of Reaction - kJ/mol_rxn
        dh_rxn_dict = {"R1": 136.5843}
        self.dh_rxn = Param(self.rate_reaction_idx,
                            initialize=dh_rxn_dict,
                            doc="Heat of reaction [kJ/mol]")

        # -------------------------------------------------------------------------
        """ Reaction properties that can be estimated"""

        # Particle grain radius within OC particle
        self.grain_radius = Var(domain=Reals,
                                initialize=2.6e-7,
                                doc='Representative particle grain'
                                'radius within OC particle [m]')
        self.grain_radius.fix()

        # Molar density OC particle
        self.dens_mol_sol = Var(domain=Reals,
                                initialize=32811,
                                doc='Molar density of OC particle [mol/m^3]')
        self.dens_mol_sol.fix()

        # Available volume for reaction - from EPAT report (1-ep)'
        self.a_vol = Var(domain=Reals,
                         initialize=0.28,
                         doc='Available reaction vol. per vol. of OC')
        self.a_vol.fix()

        # Activation Energy
        self.energy_activation = Var(self.rate_reaction_idx,
                                     domain=Reals,
                                     initialize=4.9e1,
                                     doc='Activation energy [kJ/mol]')
        self.energy_activation.fix()

        # Reaction order
        self.rxn_order = Var(self.rate_reaction_idx,
                             domain=Reals,
                             initialize=1.3,
                             doc='Reaction order in gas species [-]')
        self.rxn_order.fix()

        # Pre-exponential factor
        self.k0_rxn = Var(self.rate_reaction_idx,
                          domain=Reals,
                          initialize=8e-4,
                          doc='Pre-exponential factor'
                          '[mol^(1-N_reaction)m^(3*N_reaction -2)/s]')
        self.k0_rxn.fix()

    @classmethod
    def define_metadata(cls, obj):
        obj.add_properties({
            'k_rxn': {
                'method': '_k_rxn',
                'units': 'mol^(1-N_reaction)m^(3*N_reaction -2)/s]'
            },
            'OC_conv': {
                'method': "_OC_conv",
                'units': None
            },
            'OC_conv_temp': {
                'method': "_OC_conv_temp",
                'units': None
            },
            'reaction_rate': {
                'method': "_reaction_rate",
                'units': 'mol_rxn/m3.s'
            }
        })
        obj.add_default_units({
            'time': 's',
            'length': 'm',
            'mass': 'kg',
            'amount': 'mol',
            'temperature': 'K',
            'energy': 'kJ'
        })
Exemple #25
0
class IdealParameterData(PhysicalParameterBlock):
    """
    Property Parameter Block Class
    Contains parameters and indexing sets associated with properties for
    BTX system.
    """
    # Config block for the _IdealStateBlock
    CONFIG = PhysicalParameterBlock.CONFIG()

    CONFIG.declare(
        "valid_phase",
        ConfigValue(default=('Vap', 'Liq'),
                    domain=In(['Liq', 'Vap', ('Vap', 'Liq'), ('Liq', 'Vap')]),
                    description="Flag indicating the valid phase",
                    doc="""Flag indicating the valid phase for a given set of
conditions, and thus corresponding constraints  should be included,
**default** - ('Vap', 'Liq').
**Valid values:** {
**'Liq'** - Liquid only,
**'Vap'** - Vapor only,
**('Vap', 'Liq')** - Vapor-liquid equilibrium,
**('Liq', 'Vap')** - Vapor-liquid equilibrium,}"""))

    def build(self):
        '''
        Callable method for Block construction.
        '''
        super(IdealParameterData, self).build()

        self.state_block_class = IdealStateBlock

        # List of valid phases in property package
        if self.config.valid_phase == ('Liq', 'Vap') or \
                self.config.valid_phase == ('Vap', 'Liq'):
            self.phase_list = Set(initialize=['Liq', 'Vap'], ordered=True)
        elif self.config.valid_phase == 'Liq':
            self.phase_list = Set(initialize=['Liq'])
        else:
            self.phase_list = Set(initialize=['Vap'])

    @classmethod
    def define_metadata(cls, obj):
        """Define properties supported and units."""
        obj.add_properties({
            'flow_mol': {
                'method': None,
                'units': 'mol/s'
            },
            'mole_frac_comp': {
                'method': None,
                'units': 'none'
            },
            'temperature': {
                'method': None,
                'units': 'K'
            },
            'pressure': {
                'method': None,
                'units': 'Pa'
            },
            'flow_mol_phase': {
                'method': None,
                'units': 'mol/s'
            },
            'dens_mol_phase': {
                'method': '_dens_mol_phase',
                'units': 'mol/m^3'
            },
            'pressure_sat': {
                'method': '_pressure_sat',
                'units': 'Pa'
            },
            'mole_frac_phase_comp': {
                'method': '_mole_frac_phase',
                'units': 'no unit'
            },
            'energy_internal_mol_phase_comp': {
                'method': '_energy_internal_mol_phase_comp',
                'units': 'J/mol'
            },
            'energy_internal_mol_phase': {
                'method': '_enenrgy_internal_mol_phase',
                'units': 'J/mol'
            },
            'enth_mol_phase_comp': {
                'method': '_enth_mol_phase_comp',
                'units': 'J/mol'
            },
            'enth_mol_phase': {
                'method': '_enth_mol_phase',
                'units': 'J/mol'
            },
            'entr_mol_phase_comp': {
                'method': '_entr_mol_phase_comp',
                'units': 'J/mol'
            },
            'entr_mol_phase': {
                'method': '_entr_mol_phase',
                'units': 'J/mol'
            },
            'temperature_bubble': {
                'method': '_temperature_bubble',
                'units': 'K'
            },
            'temperature_dew': {
                'method': '_temperature_dew',
                'units': 'K'
            },
            'pressure_bubble': {
                'method': '_pressure_bubble',
                'units': 'Pa'
            },
            'pressure_dew': {
                'method': '_pressure_dew',
                'units': 'Pa'
            },
            'fug_vap': {
                'method': '_fug_vap',
                'units': 'Pa'
            },
            'fug_liq': {
                'method': '_fug_liq',
                'units': 'Pa'
            },
            'dh_vap': {
                'method': '_dh_vap',
                'units': 'J/mol'
            },
            'ds_vap': {
                'method': '_ds_vap',
                'units': 'J/mol.K'
            }
        })

        obj.add_default_units({
            'time': 's',
            'length': 'm',
            'mass': 'g',
            'amount': 'mol',
            'temperature': 'K',
            'energy': 'J',
            'holdup': 'mol'
        })
class ReverseOsmosis1DData(_ReverseOsmosisBaseData):
    """Standard 1D Reverse Osmosis Unit Model Class."""

    CONFIG = _ReverseOsmosisBaseData.CONFIG()

    CONFIG.declare(
        "area_definition",
        ConfigValue(
            default=DistributedVars.uniform,
            domain=In(DistributedVars),
            description="Argument for defining form of area variable",
            doc="""Argument defining whether area variable should be spatially
    variant or not. **default** - DistributedVars.uniform.
    **Valid values:** {
    DistributedVars.uniform - area does not vary across spatial domain,
    DistributedVars.variant - area can vary over the domain and is indexed
    by time and space.}"""))

    CONFIG.declare(
        "transformation_method",
        ConfigValue(
            default=useDefault,
            description="Discretization method to use for DAE transformation",
            doc="""Discretization method to use for DAE transformation. See Pyomo
    documentation for supported transformations."""))

    CONFIG.declare(
        "transformation_scheme",
        ConfigValue(
            default=useDefault,
            description="Discretization scheme to use for DAE transformation",
            doc="""Discretization scheme to use when transforming domain. See
    Pyomo documentation for supported schemes."""))

    CONFIG.declare(
        "finite_elements",
        ConfigValue(
            default=20,
            domain=int,
            description="Number of finite elements in length domain",
            doc="""Number of finite elements to use when discretizing length 
            domain (default=20)"""))

    CONFIG.declare(
        "collocation_points",
        ConfigValue(
            default=5,
            domain=int,
            description="Number of collocation points per finite element",
            doc="""Number of collocation points to use per finite element when
            discretizing length domain (default=5)"""))

    def _process_config(self):
        if self.config.transformation_method is useDefault:
            _log.warning("Discretization method was "
                         "not specified for the "
                         "reverse osmosis module. "
                         "Defaulting to finite "
                         "difference method.")
            self.config.transformation_method = "dae.finite_difference"

        if self.config.transformation_scheme is useDefault:
            _log.warning("Discretization scheme was "
                         "not specified for the "
                         "reverse osmosis module."
                         "Defaulting to backward finite "
                         "difference.")
            self.config.transformation_scheme = "BACKWARD"

    def build(self):
        """
        Build 1D RO model (pre-DAE transformation).

        Args:
            None

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

        # Check configuration errors
        self._process_config()

        # Build 1D Control volume for feed side
        self.feed_side = feed_side = ControlVolume1DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "area_definition": self.config.area_definition,
                "property_package": self.config.property_package,
                "property_package_args": self.config.property_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
            })

        # Add geometry to feed side
        feed_side.add_geometry()
        # Add state blocks to feed side
        feed_side.add_state_blocks(has_phase_equilibrium=False)
        # Populate feed side
        feed_side.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_mass_transfer=True)
        feed_side.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)
        # Apply transformation to feed side
        feed_side.apply_transformation()
        add_object_reference(self, 'length_domain',
                             self.feed_side.length_domain)
        self.first_element = self.length_domain.first()
        self.difference_elements = Set(ordered=True,
                                       initialize=(x
                                                   for x in self.length_domain
                                                   if x != self.first_element))

        # Add inlet/outlet ports for feed side
        self.add_inlet_port(name="inlet", block=feed_side)
        self.add_outlet_port(name="retentate", block=feed_side)
        # Make indexed stateblock and separate stateblock for permeate-side and permeate outlet, respectively.
        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  # these blocks are not inlets
        self.permeate_side = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            self.length_domain,
            doc="Material properties of permeate along permeate channel",
            default=tmp_dict)
        self.mixed_permeate = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            doc="Material properties of mixed permeate exiting the module",
            default=tmp_dict)

        # Membrane interface: indexed state block
        self.feed_side.properties_interface = self.config.property_package.state_block_class(
            self.flowsheet().config.time,
            self.length_domain,
            doc="Material properties of feed-side membrane interface",
            default=tmp_dict)

        # Add port to mixed_permeate
        self.add_port(name="permeate", block=self.mixed_permeate)

        # ==========================================================================
        """ Add references to control volume geometry."""
        add_object_reference(self, 'length', feed_side.length)
        add_object_reference(self, 'area_cross', feed_side.area)

        # Add reference to pressure drop for feed side only
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            add_object_reference(self, 'dP_dx', feed_side.deltaP)

        self._make_performance()

        self._add_expressions()

    def _make_performance(self):
        """
        Variables and constraints for unit model.

        Args:
            None

        Returns:
            None
        """

        solvent_set = self.config.property_package.solvent_set
        solute_set = self.config.property_package.solute_set

        # Units
        units_meta = \
            self.config.property_package.get_metadata().get_derived_units

        # ==========================================================================

        self.width = Var(initialize=1,
                         bounds=(1e-1, 1e3),
                         domain=NonNegativeReals,
                         units=units_meta('length'),
                         doc='Membrane width')

        super()._make_performance()

        # mass transfer
        def mass_transfer_phase_comp_initialize(b, t, x, p, j):
            return value(
                self.feed_side.properties[t, x].get_material_flow_terms(
                    'Liq', j) * self.recovery_mass_phase_comp[t, 'Liq', j])

        self.mass_transfer_phase_comp = Var(
            self.flowsheet().config.time,
            self.length_domain,
            self.config.property_package.phase_list,
            self.config.property_package.component_list,
            initialize=mass_transfer_phase_comp_initialize,
            bounds=(1e-8, 1e6),
            domain=NonNegativeReals,
            units=units_meta('mass') * units_meta('time')**-1 *
            units_meta('length')**-1,
            doc='Mass transfer to permeate')

        if self.config.has_pressure_change:
            self.deltaP = Var(self.flowsheet().config.time,
                              initialize=-1e5,
                              bounds=(-1e6, 0),
                              domain=NegativeReals,
                              units=units_meta('pressure'),
                              doc='Pressure drop across unit')

        # ==========================================================================
        # Mass transfer term equation

        @self.Constraint(self.flowsheet().config.time,
                         self.difference_elements,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer term")
        def eq_mass_transfer_term(b, t, x, p, j):
            return b.mass_transfer_phase_comp[
                t, x, p, j] == -b.feed_side.mass_transfer_term[t, x, p, j]

        # ==========================================================================
        # Mass flux = feed mass transfer equation

        @self.Constraint(self.flowsheet().config.time,
                         self.difference_elements,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer term")
        def eq_mass_flux_equal_mass_transfer(b, t, x, p, j):
            return b.flux_mass_phase_comp[
                t, x, p,
                j] * b.width == -b.feed_side.mass_transfer_term[t, x, p, j]

        # ==========================================================================
        # Mass flux equations (Jw and Js)

        # ==========================================================================
        # Final permeate mass flow rate (of solvent and solute) --> Mp,j, final = sum(Mp,j)

        @self.Constraint(self.flowsheet().config.time,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Permeate mass flow rates exiting unit")
        def eq_permeate_production(b, t, p, j):
            return (b.mixed_permeate[t].get_material_flow_terms(p, j) == sum(
                b.permeate_side[t, x].get_material_flow_terms(p, j)
                for x in b.difference_elements))

        # ==========================================================================
        # Feed and permeate-side mass transfer connection --> Mp,j = Mf,transfer = Jj * W * L/n

        @self.Constraint(self.flowsheet().config.time,
                         self.difference_elements,
                         self.config.property_package.phase_list,
                         self.config.property_package.component_list,
                         doc="Mass transfer from feed to permeate")
        def eq_connect_mass_transfer(b, t, x, p, j):
            return (b.permeate_side[t, x].get_material_flow_terms(
                p, j) == -b.feed_side.mass_transfer_term[t, x, p, j] *
                    b.length / b.nfe)

        ## ==========================================================================
        # Pressure drop
        if (self.config.pressure_change_type
                == PressureChangeType.fixed_per_unit_length
                or self.config.pressure_change_type
                == PressureChangeType.calculated):

            @self.Constraint(self.flowsheet().config.time,
                             doc='Pressure drop across unit')
            def eq_pressure_drop(b, t):
                return (b.deltaP[t] == sum(b.dP_dx[t, x] * b.length / b.nfe
                                           for x in b.difference_elements))

        if (self.config.pressure_change_type
                == PressureChangeType.fixed_per_stage
                and self.config.has_pressure_change):

            @self.Constraint(self.flowsheet().config.time,
                             self.length_domain,
                             doc='Fixed pressure drop across unit')
            def eq_pressure_drop(b, t, x):
                return b.deltaP[t] == b.length * b.dP_dx[t, x]

        ## ==========================================================================
        # Feed-side isothermal conditions
        # NOTE: this could go on the feed_side block, but that seems to hurt initialization
        #       in the tests for this unit
        @self.Constraint(self.flowsheet().config.time,
                         self.difference_elements,
                         doc="Isothermal assumption for feed channel")
        def eq_feed_isothermal(b, t, x):
            return b.feed_side.properties[t, b.first_element].temperature == \
                   b.feed_side.properties[t, x].temperature

    def initialize_build(blk,
                         initialize_guess=None,
                         state_args=None,
                         outlvl=idaeslog.NOTSET,
                         solver=None,
                         optarg=None,
                         fail_on_warning=False,
                         ignore_dof=False):
        """
        Initialization routine for 1D-RO unit.

        Keyword Arguments:
            initialize_guess : a dict of guesses for solvent_recovery, solute_recovery,
                               and cp_modulus. These guesses offset the initial values
                               for the retentate, permeate, and membrane interface
                               state blocks from the inlet feed
                               (default =
                               {'deltaP': -1e4,
                               'solvent_recovery': 0.5,
                               'solute_recovery': 0.01,
                               'cp_modulus': 1.1})
            state_args : a dict of arguments to be passed to the property
                         package(s) to provide an initial state for the inlet
                         feed side state block (see documentation of the specific
                         property package) (default = None).
            outlvl : sets output level of initialization routine
            solver : str indicating which solver to use during
                     initialization (default = None, use default solver)
            optarg : solver options dictionary object (default=None, use default solver options)
            fail_on_warning : boolean argument to fail or only produce  warning upon unsuccessful solve (default=False)
            ignore_dof : boolean argument to ignore when DOF != 0 (default=False)
        Returns:
            None

        """

        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)

        source = blk.feed_side.properties[blk.flowsheet().config.time.first(),
                                          blk.first_element]
        state_args = blk._get_state_args(source, blk.mixed_permeate[0],
                                         initialize_guess, state_args)

        # ---------------------------------------------------------------------
        # Step 1: Initialize feed_side, permeate_side, and mixed_permeate blocks
        flags_feed_side = blk.feed_side.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['feed_side'],
            hold_state=True)

        init_log.info("Initialization Step 1 Complete")
        if not ignore_dof:
            check_dof(blk, fail_flag=fail_on_warning, logger=init_log)
        # ---------------------------------------------------------------------
        # Initialize other state blocks
        # base properties on inlet state block

        flag_feed_side_properties_interface = blk.feed_side.properties_interface.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['interface'])
        flags_permeate_side = blk.permeate_side.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['permeate'])
        flags_mixed_permeate = blk.mixed_permeate.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args['permeate'])
        init_log.info("Initialization Step 2 Complete.")

        # ---------------------------------------------------------------------
        # Solve unit
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
            # occasionally it might be worth retrying a solve
            if not check_optimal_termination(res):
                init_log.warn(
                    "Trouble solving ReverseOsmosis1D unit model, trying one more time"
                )
                res = opt.solve(blk, tee=slc.tee)
        check_solve(res,
                    logger=init_log,
                    fail_flag=fail_on_warning,
                    checkpoint='Initialization Step 3')
        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.feed_side.release_state(flags_feed_side, outlvl)
        init_log.info("Initialization Complete: {}".format(
            idaeslog.condition(res)))

    def calculate_scaling_factors(self):
        if iscale.get_scaling_factor(self.dens_solvent) is None:
            sf = iscale.get_scaling_factor(
                self.feed_side.properties[0, 0].dens_mass_phase['Liq'])
            iscale.set_scaling_factor(self.dens_solvent, sf)

        super().calculate_scaling_factors()

        # these variables should have user input, if not there will be a warning
        if iscale.get_scaling_factor(self.width) is None:
            sf = iscale.get_scaling_factor(self.width, default=1, warning=True)
            iscale.set_scaling_factor(self.width, sf)

        if iscale.get_scaling_factor(self.length) is None:
            sf = iscale.get_scaling_factor(self.length,
                                           default=10,
                                           warning=True)
            iscale.set_scaling_factor(self.length, sf)

        # setting scaling factors for variables

        # will not override if the user provides the scaling factor
        ## default of 1 set by ControlVolume1D
        if iscale.get_scaling_factor(self.area_cross) == 1:
            iscale.set_scaling_factor(self.area_cross, 100)

        for (t, x, p, j), v in self.mass_transfer_phase_comp.items():
            sf = (iscale.get_scaling_factor(
                self.feed_side.properties[t, x].get_material_flow_terms(p, j))
                  / iscale.get_scaling_factor(self.feed_side.length)) * value(
                      self.nfe)
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, sf)
            v = self.feed_side.mass_transfer_term[t, x, p, j]
            if iscale.get_scaling_factor(v) is None:
                iscale.set_scaling_factor(v, sf)

        if hasattr(self, 'deltaP'):
            for v in self.deltaP.values():
                if iscale.get_scaling_factor(v) is None:
                    iscale.set_scaling_factor(v, 1e-4)

        if hasattr(self, 'dP_dx'):
            for v in self.feed_side.pressure_dx.values():
                iscale.set_scaling_factor(v, 1e-5)
        else:
            for v in self.feed_side.pressure_dx.values():
                iscale.set_scaling_factor(v, 1e5)
Exemple #27
0
class TranslatorData(UnitModelBlockData):
    """
    Standard Translator Block Class
    """

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

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

        Args:
            None

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

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

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

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

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

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

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

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

        # Create solver
        opt = get_solver(solver, optarg)

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

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

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

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

        blk.properties_in.release_state(flags=flags, outlvl=outlvl)
Exemple #28
0
class PhysicalParameterData(PhysicalParameterBlock):
    """
    Property Parameter Block Class.
    """

    # Config block for the _IdealStateBlock
    CONFIG = PhysicalParameterBlock.CONFIG()

    CONFIG.declare("valid_phase", ConfigValue(
        default=('Vap', 'Liq'),
        domain=In(['Liq', 'Vap', ('Vap', 'Liq'), ('Liq', 'Vap')]),
        description="Flag indicating the valid phase",
        doc="""Flag indicating the valid phase for a given set of
    conditions, and thus corresponding constraints  should be included,
    **default** - ('Vap', 'Liq').
    **Valid values:** {
    **'Liq'** - Liquid only,
    **'Vap'** - Vapor only,
    **('Vap', 'Liq')** - Vapor-liquid equilibrium,
    **('Liq', 'Vap')** - Vapor-liquid equilibrium,}"""))

    CONFIG.declare("Cp", ConfigValue(
        default=0.035,
        domain=float,
        description="Constant pressure heat capacity in MJ/(kmol K)",
        doc="""Value for the constant pressure heat capacity,
        **default** = 0.035 MJ/(kmol K)"""))

    def build(self):
        '''
        Callable method for Block construction.
        '''
        super(PhysicalParameterData, self).build()

        self._state_block_class = IdealStateBlock

        # List of valid phases and components in property package

        if self.config.valid_phase == ('Liq', 'Vap') or \
                self.config.valid_phase == ('Vap', 'Liq'):
            self.Liq = LiquidPhase()
            self.Vap = VaporPhase()
        elif self.config.valid_phase == 'Liq':
            self.Liq = LiquidPhase()
        else:
            self.Vap = VaporPhase()

        self.CH4 = Component()
        self.CO = Component()
        self.H2 = Component()
        self.CH3OH = Component()

        self.phase_equilibrium_idx = Set(initialize=[1, 2, 3, 4])

        self.phase_equilibrium_list = \
            {1: ["CH4", ("Vap", "Liq")],
             2: ["CO", ("Vap", "Liq")],
             3: ["H2", ("Vap", "Liq")],
             4: ["CH3OH", ("Vap", "Liq")]}

        # Antoine coefficients assume pressure in mmHG and temperature in K
        self.vapor_pressure_coeff = {('CH4', 'A'): 15.2243,
                                     ('CH4', 'B'): 897.84,
                                     ('CH4', 'C'): -7.16,
                                     ('CO', 'A'): 14.3686,
                                     ('CO', 'B'): 530.22,
                                     ('CO', 'C'): -13.15,
                                     ('H2', 'A'): 13.6333,
                                     ('H2', 'B'): 164.9,
                                     ('H2', 'C'): 3.19,
                                     ('CH3OH', 'A'): 18.5875,
                                     ('CH3OH', 'B'): 3626.55,
                                     ('CH3OH', 'C'): -34.29}

        Cp = self.config.Cp
        Cv = value(Cp - pyunits.convert(Constants.gas_constant,
                                        pyunits.MJ/pyunits.kmol/pyunits.K))
        gamma = Cp / Cv

        self.gamma = Param(within=NonNegativeReals,
                           mutable=True,
                           default=gamma,
                           doc='Ratio of Cp to Cv')

        self.Cp = Param(within=NonNegativeReals,
                        mutable=True,
                        default=Cp,
                        units=pyunits.MJ/pyunits.kmol/pyunits.K,
                        doc='Constant pressure heat capacity')

    @classmethod
    def define_metadata(cls, obj):
        """Define properties supported and units."""
        obj.add_properties(
            {'flow_mol': {'method': None},
             'mole_frac': {'method': None},
             'temperature': {'method': None},
             'pressure': {'method': None},
             'flow_mol_phase': {'method': None},
             'density_mol': {'method': '_density_mol'},
             'vapor_pressure': {'method': '_vapor_pressure'},
             'mole_frac_phase': {'method': '_mole_frac_phase'},
             'enthalpy_comp_liq': {'method': '_enthalpy_comp_liq'},
             'enthalpy_comp_vap': {'method': '_enthalpy_comp_vap'},
             'enthalpy_liq': {'method': '_enthalpy_liq'},
             'enthalpy_vap': {'method': '_enthalpy_vap'}})

        obj.add_default_units({'time': pyunits.s,
                               'length': pyunits.m,
                               'mass': pyunits.Gg,  # yields base units MJ, MPa
                               'amount': pyunits.kmol,
                               'temperature': pyunits.hK})
Exemple #29
0
def _make_heater_config_block(config):
    """
    Declare configuration options for HeaterData block.
    """
    config.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.componentPhase,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.componentPhase.
**Valid values:** {
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    config.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.enthalpyTotal,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single ethalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - ethalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    config.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    config.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Phase equilibrium construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed, **default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}"""))
    config.declare(
        "has_pressure_change",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
constructed,
**default** - False.
**Valid values:** {
**True** - include pressure change terms,
**False** - exclude pressure change terms.}"""))
    config.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}"""))
    config.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
Exemple #30
0
def _add_OA_configs(CONFIG):
    CONFIG.declare("init_strategy", ConfigValue(
        default="set_covering", domain=In(valid_init_strategies.keys()),
        description="Initialization strategy to use.",
        doc="Selects the initialization strategy to use when generating "
            "the initial cuts to construct the master problem."
    ))
    CONFIG.declare("custom_init_disjuncts", ConfigList(
        # domain=ComponentSets of Disjuncts,
        default=None,
        description="List of disjunct sets to use for initialization."
    ))
    CONFIG.declare("max_slack", ConfigValue(
        default=1000, domain=NonNegativeFloat,
        description="Upper bound on slack variables for OA"
    ))
    CONFIG.declare("OA_penalty_factor", ConfigValue(
        default=1000, domain=NonNegativeFloat,
        description="Penalty multiplication term for slack variables on the "
                    "objective value."
    ))
    CONFIG.declare("set_cover_iterlim", ConfigValue(
        default=8, domain=NonNegativeInt,
        description="Limit on the number of set covering iterations."
    ))
    CONFIG.declare("call_before_master_solve", ConfigValue(
        default=_DoNothing,
        description="callback hook before calling the master problem solver"
    ))
    CONFIG.declare("call_after_master_solve", ConfigValue(
        default=_DoNothing,
        description="callback hook after a solution of the master problem"
    ))
    CONFIG.declare("call_before_subproblem_solve", ConfigValue(
        default=_DoNothing,
        description="callback hook before calling the subproblem solver"
    ))
    CONFIG.declare("call_after_subproblem_solve", ConfigValue(
        default=_DoNothing,
        description="callback hook after a solution of the "
                    "nonlinear subproblem"
    ))
    CONFIG.declare("call_after_subproblem_feasible", ConfigValue(
        default=_DoNothing,
        description="callback hook after feasible solution of "
                    "the nonlinear subproblem"
    ))
    CONFIG.declare("algorithm_stall_after", ConfigValue(
        default=2,
        description="number of non-improving master iterations after which "
                    "the algorithm will stall and exit."
    ))
    CONFIG.declare("round_discrete_vars", ConfigValue(
        default=True,
        description="flag to round subproblem discrete variable values to the "
        "nearest integer. Rounding is done before fixing disjuncts."
    ))
    CONFIG.declare("force_subproblem_nlp", ConfigValue(
        default=False,
        description="Force subproblems to be NLP, even if discrete variables "
        "exist."
    ))
    CONFIG.declare("mip_presolve", ConfigValue(
        default=True,
        description="Flag to enable or diable GDPopt MIP presolve. "
        "Default=True.",
        domain=bool
    ))
    CONFIG.declare("subproblem_presolve", ConfigValue(
        default=True,
        description="Flag to enable or disable subproblem presolve. "
        "Default=True.",
        domain=bool
    ))
    CONFIG.declare("max_fbbt_iterations", ConfigValue(
        default=3,
        description="Maximum number of feasibility-based bounds tightening "
        "iterations to do during NLP subproblem preprocessing.",
        domain=PositiveInt
    ))
    CONFIG.declare("tighten_nlp_var_bounds", ConfigValue(
        default=False,
        description="Whether or not to do feasibility-based bounds tightening "
        "on the variables in the NLP subproblem before solving it.",
        domain=bool
    ))
    CONFIG.declare("calc_disjunctive_bounds", ConfigValue(
        default=False,
        description="Calculate special disjunctive variable bounds for GLOA. "
        "False by default.",
        domain=bool
    ))
    CONFIG.declare("obbt_disjunctive_bounds", ConfigValue(
        default=False,
        description="Use optimality-based bounds tightening rather than "
        "feasibility-based bounds tightening to compute disjunctive variable "
        "bounds. False by default.",
        domain=bool
    ))
    return CONFIG