예제 #1
0
class HeatExchangerData(UnitModelBlockData):
    """
    Simple 0D heat exchange unit.
    Unit model to transfer heat from one material to another.
    """
    CONFIG = UnitModelBlockData.CONFIG()
    _make_heat_exchanger_config(CONFIG)

    def set_scaling_factor_energy(self, f):
        """
        This function sets scaling_factor_energy for both side_1 and side_2.
        This factor multiplies the energy balance and heat transfer equations
        in the heat exchnager.  The value of this factor should be about
        1/(expected heat duty).

        Args:
            f: Energy balance scaling factor
        """
        self.side_1.scaling_factor_energy.value = f
        self.side_2.scaling_factor_energy.value = f

    def build(self):
        """
        Building model

        Args:
            None
        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super().build()
        config = self.config
        # Add variables
        self.overall_heat_transfer_coefficient = Var(
            self.flowsheet().config.time,
            domain=PositiveReals,
            initialize=100,
            doc="Overall heat transfer coefficient")
        self.overall_heat_transfer_coefficient.latex_symbol = "U"
        self.area = Var(domain=PositiveReals,
                        initialize=1000,
                        doc="Heat exchange area")
        self.area.latex_symbol = "A"
        if config.flow_pattern == HeatExchangerFlowPattern.crossflow:
            self.crossflow_factor = Var(
                self.flowsheet().config.time,
                initialize=1,
                doc="Factor to adjust coutercurrent flow heat transfer "
                "calculation for cross flow.")

        if config.delta_temperature_rule == delta_temperature_underwood2_rule:
            # Define a cube root function that return the real negative root
            # for the cube root of a negative number.
            self.cbrt = ExternalFunction(library=functions_lib(),
                                         function="cbrt")

        # Add Control Volumes
        _make_heater_control_volume(self,
                                    "side_1",
                                    config.side_1,
                                    dynamic=config.dynamic,
                                    has_holdup=config.has_holdup)
        _make_heater_control_volume(self,
                                    "side_2",
                                    config.side_2,
                                    dynamic=config.dynamic,
                                    has_holdup=config.has_holdup)
        # Add Ports
        self.add_inlet_port(name="inlet_1", block=self.side_1)
        self.add_inlet_port(name="inlet_2", block=self.side_2)
        self.add_outlet_port(name="outlet_1", block=self.side_1)
        self.add_outlet_port(name="outlet_2", block=self.side_2)
        # Add convienient references to heat duty.
        add_object_reference(self, "heat_duty", self.side_2.heat)
        self.side_1.heat.latex_symbol = "Q_1"
        self.side_2.heat.latex_symbol = "Q_2"

        @self.Expression(self.flowsheet().config.time,
                         doc="Temperature difference at the side 1 inlet end")
        def delta_temperature_in(b, t):
            if b.config.flow_pattern == \
                    HeatExchangerFlowPattern.countercurrent:
                return b.side_1.properties_in[t].temperature -\
                       b.side_2.properties_out[t].temperature
            elif b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent:
                return b.side_1.properties_in[t].temperature -\
                       b.side_2.properties_in[t].temperature
            elif b.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
                return b.side_1.properties_in[t].temperature -\
                       b.side_2.properties_out[t].temperature
            else:
                raise ConfigurationError(
                    "Flow pattern {} not supported".format(
                        b.config.flow_pattern))

        @self.Expression(self.flowsheet().config.time,
                         doc="Temperature difference at the side 1 outlet end")
        def delta_temperature_out(b, t):
            if b.config.flow_pattern == \
                    HeatExchangerFlowPattern.countercurrent:
                return b.side_1.properties_out[t].temperature -\
                       b.side_2.properties_in[t].temperature
            elif b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent:
                return b.side_1.properties_out[t].temperature -\
                       b.side_2.properties_out[t].temperature
            elif b.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
                return b.side_1.properties_out[t].temperature -\
                       b.side_2.properties_in[t].temperature

        # Add a unit level energy balance
        def unit_heat_balance_rule(b, t):
            return 0 == self.side_1.heat[t] + self.side_2.heat[t]

        self.unit_heat_balance = Constraint(self.flowsheet().config.time,
                                            rule=unit_heat_balance_rule)
        # Add heat transfer equation
        self.delta_temperature = Expression(
            self.flowsheet().config.time,
            rule=config.delta_temperature_rule,
            doc="Temperature difference driving force for heat transfer")
        self.delta_temperature.latex_symbol = "\\Delta T"

        if config.flow_pattern == HeatExchangerFlowPattern.crossflow:
            self.heat_transfer_equation = Constraint(
                self.flowsheet().config.time,
                rule=_cross_flow_heat_transfer_rule)
        else:
            self.heat_transfer_equation = Constraint(
                self.flowsheet().config.time, rule=_heat_transfer_rule)

    def initialize(self,
                   state_args_1=None,
                   state_args_2=None,
                   outlvl=0,
                   solver='ipopt',
                   optarg={'tol': 1e-6},
                   duty=1000):
        """
        Heat exchanger initialization method.

        Args:
            state_args_1 : a dict of arguments to be passed to the property
                initialization for side_1 (see documentation of the specific
                property package) (default = {}).
            state_args_2 : a dict of arguments to be passed to the property
                initialization for side_2 (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 which solver to use during
                     initialization (default = 'ipopt')
            duty : an initial guess for the amount of heat transfered
                (default = 10000)

        Returns:
            None

        """
        # Set solver options
        tee = True if outlvl >= 3 else False
        opt = SolverFactory(solver)
        opt.options = optarg
        flags1 = self.side_1.initialize(outlvl=outlvl - 1,
                                        optarg=optarg,
                                        solver=solver,
                                        state_args=state_args_1)

        if outlvl > 0:
            _log.info('{} Initialization Step 1a (side_1) Complete.'.format(
                self.name))

        flags2 = self.side_2.initialize(outlvl=outlvl - 1,
                                        optarg=optarg,
                                        solver=solver,
                                        state_args=state_args_2)

        if outlvl > 0:
            _log.info('{} Initialization Step 1b (side_2) Complete.'.format(
                self.name))
        # ---------------------------------------------------------------------
        # Solve unit without heat transfer equation
        self.heat_transfer_equation.deactivate()
        self.side_2.heat.fix(duty)
        results = opt.solve(self, tee=tee, symbolic_solver_labels=True)
        if outlvl > 0:
            if results.solver.termination_condition == \
                    TerminationCondition.optimal:
                _log.info('{} Initialization Step 2 Complete.'.format(
                    self.name))
            else:
                _log.warning('{} Initialization Step 2 Failed.'.format(
                    self.name))
        self.side_2.heat.unfix()
        self.heat_transfer_equation.activate()
        # ---------------------------------------------------------------------
        # Solve unit
        results = opt.solve(self, tee=tee, symbolic_solver_labels=True)
        if outlvl > 0:
            if results.solver.termination_condition == \
                    TerminationCondition.optimal:
                _log.info('{} Initialization Step 3 Complete.'.format(
                    self.name))
            else:
                _log.warning('{} Initialization Step 3 Failed.'.format(
                    self.name))
        # ---------------------------------------------------------------------
        # Release Inlet state
        self.side_1.release_state(flags1, outlvl - 1)
        self.side_2.release_state(flags2, outlvl - 1)

        if outlvl > 0:
            _log.info('{} Initialization Complete.'.format(self.name))
예제 #2
0
class CondenserData(UnitModelBlockData):
    """
    Condenser unit for distillation model.
    Unit model to condense (total/partial) the vapor from the top tray of
    the distillation column.
    """
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare(
        "condenser_type",
        ConfigValue(
            default=CondenserType.totalCondenser,
            domain=In(CondenserType),
            description="Type of condenser flag",
            doc="""Indicates what type of condenser should be constructed,
**default** - CondenserType.totalCondenser.
**Valid values:** {
**CondenserType.totalCondenser** - Incoming vapor from top tray is condensed
to all liquid,
**CondenserType.partialCondenser** - Incoming vapor from top tray is
partially condensed to a vapor and liquid stream.}"""))
    CONFIG.declare(
        "temperature_spec",
        ConfigValue(default=None,
                    domain=In(TemperatureSpec),
                    description="Temperature spec for the condenser",
                    doc="""Temperature specification for the condenser,
**default** - TemperatureSpec.none
**Valid values:** {
**TemperatureSpec.none** - No spec is selected,
**TemperatureSpec.atBubblePoint** - Condenser temperature set at
bubble point i.e. total condenser,
**TemperatureSpec.customTemperature** - Condenser temperature at
user specified temperature.}"""))
    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.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.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.enthalpyTotal.
**Valid values:** {
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare(
        "has_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):
        """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(CondenserData, self).build()

        # Check config arguments
        if self.config.temperature_spec is None:
            raise ConfigurationError("temperature_spec config argument "
                                     "has not been specified. Please select "
                                     "a valid option.")
        if (self.config.condenser_type == CondenserType.partialCondenser) and \
                (self.config.temperature_spec ==
                 TemperatureSpec.atBubblePoint):
            raise ConfigurationError("condenser_type set to partial but "
                                     "temperature_spec set to atBubblePoint. "
                                     "Select customTemperature and specify "
                                     "outlet temperature.")

        # Add Control Volume for the condenser
        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_state_blocks(has_phase_equilibrium=True)

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

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

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

        # 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._make_ports()

        if self.config.condenser_type == CondenserType.totalCondenser:

            self._make_splits_total_condenser()

            if (self.config.temperature_spec == TemperatureSpec.atBubblePoint):
                # Option 1: if true, condition for total condenser
                # (T_cond = T_bubble)
                # Option 2: if this is false, then user has selected
                # custom temperature spec and needs to fix an outlet
                # temperature.
                def rule_total_cond(self, t):
                    return self.control_volume.properties_out[t].\
                        temperature == self.control_volume.properties_out[t].\
                        temperature_bubble

                self.eq_total_cond_spec = Constraint(self.flowsheet().time,
                                                     rule=rule_total_cond)

        else:
            self._make_splits_partial_condenser()

        # Add object reference to variables of the control volume
        # Reference to the heat duty
        self.heat_duty = Reference(self.control_volume.heat[:])

        # Reference to the pressure drop (if set to True)
        if self.config.has_pressure_change:
            self.deltaP = Reference(self.control_volume.deltaP[:])

    def _make_ports(self):

        # Add Ports for the condenser
        # Inlet port (the vapor from the top tray)
        self.add_inlet_port()

        # Outlet ports that always exist irrespective of condenser type
        self.reflux = Port(noruleinit=True,
                           doc="Reflux stream that is"
                           " returned to the top tray.")
        self.distillate = Port(noruleinit=True,
                               doc="Distillate stream that is"
                               " the top product.")

        if self.config.condenser_type == CondenserType.partialCondenser:
            self.vapor_outlet = Port(noruleinit=True,
                                     doc="Vapor outlet port from a "
                                     "partial condenser")
        # Add codnenser specific variables
        self.reflux_ratio = Var(initialize=1,
                                doc="Reflux ratio for the condenser")

    def _make_splits_total_condenser(self):

        # Get dict of Port members and names
        member_list = self.control_volume.\
            properties_out[0].define_port_members()

        # Create references and populate the reflux, distillate ports
        for k in member_list:
            # Create references and populate the intensive variables
            if "flow" not in k:
                if not member_list[k].is_indexed():
                    var = self.control_volume.properties_out[:].\
                        component(member_list[k].local_name)
                else:
                    var = self.control_volume.properties_out[:].\
                        component(member_list[k].local_name)[...]

                # add the reference and variable name to the reflux port
                self.reflux.add(Reference(var), k)

                # add the reference and variable name to the distillate port
                self.distillate.add(Reference(var), k)

            elif "flow" in k:
                # Create references and populate the extensive variables
                # This is for vars that are not indexed
                if not member_list[k].is_indexed():
                    # Expression for reflux flow and relation to the
                    # reflux_ratio variable

                    def rule_reflux_flow(self, t):
                        return self.control_volume.properties_out[t].\
                            component(member_list[k].local_name) * \
                            (self.reflux_ratio / (1 + self.reflux_ratio))

                    self.e_reflux_flow = Expression(self.flowsheet().time,
                                                    rule=rule_reflux_flow)
                    self.reflux.add(self.e_reflux_flow, k)

                    # Expression for distillate flow and relation to the
                    # reflux_ratio variable
                    def rule_distillate_flow(self, t):
                        return self.control_volume.properties_out[t].\
                            component(member_list[k].local_name) / \
                            (1 + self.reflux_ratio)

                    self.e_distillate_flow = Expression(
                        self.flowsheet().time, rule=rule_distillate_flow)
                    self.distillate.add(self.e_distillate_flow, k)
                else:
                    # Create references and populate the extensive variables
                    # This is for vars that are indexed by phase, comp or both.
                    index_set = member_list[k].index_set()

                    def rule_reflux_flow(self, t, *args):
                        return self.control_volume.properties_out[t].\
                            component(member_list[k].local_name)[args] * \
                            (self.reflux_ratio / (1 + self.reflux_ratio))

                    self.e_reflux_flow = Expression(self.flowsheet().time,
                                                    index_set,
                                                    rule=rule_reflux_flow)
                    self.reflux.add(self.e_reflux_flow, k)

                    def rule_distillate_flow(self, t, *args):
                        return self.control_volume.properties_out[t].\
                            component(member_list[k].local_name)[args] / \
                            (1 + self.reflux_ratio)

                    self.e_distillate_flow = Expression(
                        self.flowsheet().time,
                        index_set,
                        rule=rule_distillate_flow)
                    self.distillate.add(self.e_distillate_flow, k)

            else:
                raise PropertyNotSupportedError(
                    "Unrecognized names for flow variables encountered while "
                    "building the condenser ports.")

    def _make_splits_partial_condenser(self):
        # Get dict of Port members and names
        member_list = self.control_volume.\
            properties_out[0].define_port_members()

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

                # add the reference and variable name to the reflux port
                self.reflux.add(Reference(var), k)

                # add the reference and variable name to the distillate port
                self.distillate.add(Reference(var), k)

                # add the reference and variable name to the
                # vapor outlet port
                self.vapor_outlet.add(Reference(var), k)

            elif "frac" in k:

                # 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 k:
                    if "mole" in k:  # 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.control_volume.properties_out[0],
                                   "mole_frac_phase_comp") and \
                            hasattr(self.control_volume.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.control_volume.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 k:
                        if hasattr(self.control_volume.properties_out[0],
                                   "mass_frac_phase_comp") and \
                            hasattr(self.control_volume.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.control_volume.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 liquid phase mole fraction
                    def rule_liq_frac(self, t, i):
                        if not flow_phase_comp:
                            sum_flow_comp = sum(
                                self.control_volume.properties_out[t].
                                component(local_name_frac)[p, i] *
                                self.control_volume.properties_out[t].
                                component(local_name_flow)[p]
                                for p in self._liquid_set)

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

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

                    self.e_liq_frac = Expression(self.flowsheet().time,
                                                 index_set,
                                                 rule=rule_liq_frac)

                    # Rule for vapor phase mass/mole fraction
                    def rule_vap_frac(self, t, i):
                        if not flow_phase_comp:
                            sum_flow_comp = sum(
                                self.control_volume.properties_out[t].
                                component(local_name_frac)[p, i] *
                                self.control_volume.properties_out[t].
                                component(local_name_flow)[p]
                                for p in self._vapor_set)
                            return sum_flow_comp / sum(
                                self.control_volume.properties_out[t].
                                component(local_name_flow)[p]
                                for p in self._vapor_set)
                        else:
                            sum_flow_comp = sum(
                                self.control_volume.properties_out[t].
                                component(local_name_flow)[p, i]
                                for p in self._vapor_set)

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

                    self.e_vap_frac = Expression(self.flowsheet().time,
                                                 index_set,
                                                 rule=rule_vap_frac)

                    # add the reference and variable name to the reflux port
                    self.reflux.add(self.e_liq_frac, k)

                    # add the reference and variable name to the
                    # distillate port
                    self.distillate.add(self.e_liq_frac, k)

                    # add the reference and variable name to the
                    # vapor port
                    self.vapor_outlet.add(self.e_vap_frac, 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.control_volume.properties_out[:].\
                        component(member_list[k].local_name)[...]

                    # add the reference and variable name to the reflux port
                    self.reflux.add(Reference(var), k)

                    # add the reference and variable name to the distillate port
                    self.distillate.add(Reference(var), k)
            elif "flow" in k:
                if "phase" not in k:

                    # 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 = str(member_list[k].local_name) + \
                            "_phase"

                        # Rule for vap phase flow
                        def rule_vap_flow(self, t):
                            return sum(self.control_volume.properties_out[t].
                                       component(local_name)[p]
                                       for p in self._vapor_set)

                        self.e_vap_flow = Expression(self.flowsheet().time,
                                                     rule=rule_vap_flow)

                        # Rule to link the liq phase flow to the reflux
                        def rule_reflux_flow(self, t):
                            return sum(self.control_volume.properties_out[t].
                                       component(local_name)[p]
                                       for p in self._liquid_set) * \
                                (self.reflux_ratio / (1 + self.reflux_ratio))

                        self.e_reflux_flow = Expression(self.flowsheet().time,
                                                        rule=rule_reflux_flow)

                        # Rule to link the liq flow to the distillate
                        def rule_distillate_flow(self, t):
                            return sum(self.control_volume.properties_out[t].
                                       component(local_name)[p]
                                       for p in self._liquid_set) / \
                                (1 + self.reflux_ratio)

                        self.e_distillate_flow = Expression(
                            self.flowsheet().time, rule=rule_distillate_flow)

                    else:
                        # when it is flow comp indexed by component list
                        str_split = \
                            str(member_list[k].local_name).split("_")
                        if len(str_split) == 3 and str_split[-1] == "comp":
                            local_name = str_split[0] + "_" + \
                                str_split[1] + "_phase_" + "comp"

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

                        # Rule for vap phase flow to the vapor outlet
                        def rule_vap_flow(self, t, i):
                            return sum(self.control_volume.properties_out[t].
                                       component(local_name)[p, i]
                                       for p in self._vapor_set)

                        self.e_vap_flow = Expression(self.flowsheet().time,
                                                     index_set,
                                                     rule=rule_vap_flow)

                        # Rule to link the liq flow to the reflux
                        def rule_reflux_flow(self, t, i):
                            return sum(self.control_volume.properties_out[t].
                                       component(local_name)[p, i]
                                       for p in self._liquid_set) * \
                                (self.reflux_ratio / (1 + self.reflux_ratio))

                        self.e_reflux_flow = Expression(self.flowsheet().time,
                                                        index_set,
                                                        rule=rule_reflux_flow)

                        # Rule to link the liq flow to the distillate
                        def rule_distillate_flow(self, t, i):
                            return sum(self.control_volume.properties_out[t].
                                       component(local_name)[p, i]
                                       for p in self._liquid_set) / \
                                (1 + self.reflux_ratio)

                        self.e_distillate_flow = Expression(
                            self.flowsheet().time,
                            index_set,
                            rule=rule_distillate_flow)

                    # add the reference and variable name to the reflux port
                    self.reflux.add(self.e_reflux_flow, k)

                    # add the reference and variable name to the
                    # distillate port
                    self.distillate.add(self.e_distillate_flow, k)

                    # add the reference and variable name to the
                    # distillate port
                    self.vapor_outlet.add(self.e_vap_flow, k)
            elif "enth" in k:
                if "phase" not in k:
                    # assumes total mixture enthalpy (enth_mol or enth_mass)
                    # and hence should not be indexed by phase
                    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 = str(member_list[k].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.")

                    # NOTE:pass phase index when generating expression only
                    # when multiple liquid or vapor phases detected
                    # else ensure consistency with state vars and do not
                    # add phase index to the port members. Hence, the check
                    # for length of local liq and vap phase sets.

                    # Rule for vap enthalpy. Setting the enthalpy to the
                    # enth_mol_phase['Vap'] value from the state block
                    def rule_vap_enth(self, t):
                        return sum(
                            self.control_volume.properties_out[t].component(
                                local_name)[p] for p in self._vapor_set)

                    self.e_vap_enth = Expression(self.flowsheet().time,
                                                 rule=rule_vap_enth)

                    # Rule to link the liq enthalpy to the reflux.
                    # Setting the enthalpy to the
                    # enth_mol_phase['Liq'] value from the state block
                    def rule_reflux_enth(self, t):
                        return sum(
                            self.control_volume.properties_out[t].component(
                                local_name)[p] for p in self._liquid_set)

                    self.e_reflux_enth = Expression(self.flowsheet().time,
                                                    rule=rule_reflux_enth)

                    # Rule to link the liq flow to the distillate.
                    # Setting the enthalpy to the
                    # enth_mol_phase['Liq'] value from the state block
                    def rule_distillate_enth(self, t):
                        return sum(
                            self.control_volume.properties_out[t].component(
                                local_name)[p] for p in self._liquid_set)

                    self.e_distillate_enth = Expression(
                        self.flowsheet().time, rule=rule_distillate_enth)

                    # add the reference and variable name to the reflux port
                    self.reflux.add(self.e_reflux_enth, k)

                    # add the reference and variable name to the
                    # distillate port
                    self.distillate.add(self.e_distillate_enth, k)

                    # add the reference and variable name to the
                    # distillate port
                    self.vapor_outlet.add(self.e_vap_enth, k)
                elif "phase" in k:
                    # 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

                    # Rule for vap flow
                    if not member_list[k].is_indexed():
                        var = self.control_volume.properties_out[:].\
                            component(member_list[k].local_name)
                    else:
                        var = self.control_volume.properties_out[:].\
                            component(member_list[k].local_name)[...]

                    # add the reference and variable name to the reflux port
                    self.reflux.add(Reference(var), k)

                    # add the reference and variable name to the distillate port
                    self.distillate.add(Reference(var), k)

                    # add the reference and variable name to the
                    # vapor outlet port
                    self.vapor_outlet.add(Reference(var), k)
                else:
                    raise PropertyNotSupportedError(
                        "Unrecognized enthalpy state variable encountered "
                        "while building ports for the condenser. Only total "
                        "mixture enthalpy or enthalpy by phase are supported.")

    def initialize(self, solver=None, outlvl=idaeslog.NOTSET):

        # TODO: Fix the inlets to the condenser to the vapor flow from
        # the top tray or take it as an argument to this method.

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

        if self.config.temperature_spec == TemperatureSpec.customTemperature:
            if degrees_of_freedom(self) != 0:
                raise ConfigurationError(
                    "Degrees of freedom is not 0 during initialization. "
                    "Check if outlet temperature has been fixed in addition "
                    "to the other inputs required as customTemperature was "
                    "selected for temperature_spec config argument.")

        if self.config.condenser_type == CondenserType.totalCondenser:
            self.eq_total_cond_spec.deactivate()

        # Initialize the inlet and outlet state blocks
        self.control_volume.initialize(outlvl=outlvl)

        # Activate the total condenser spec
        if self.config.condenser_type == CondenserType.totalCondenser:
            self.eq_total_cond_spec.activate()

        if solver is not None:
            with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
                res = solver.solve(self, tee=slc.tee)
            init_log.info("Initialization Complete, {}.".format(
                idaeslog.condition(res)))
        else:
            init_log.warning(
                "Solver not provided during initialization, proceeding"
                " with deafult solver in idaes.")
            solver = get_default_solver()
            with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
                res = solver.solve(self, tee=slc.tee)
            init_log.info("Initialization Complete, {}.".format(
                idaeslog.condition(res)))

    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}

    def _get_stream_table_contents(self, time_point=0):
        stream_attributes = {}

        if self.config.condenser_type == CondenserType.totalCondenser:
            stream_dict = {
                "Inlet": "inlet",
                "Reflux": "reflux",
                "Distillate": "distillate"
            }
        else:
            stream_dict = {
                "Inlet": "inlet",
                "Vapor Outlet": "vapor_outlet",
                "Reflux": "reflux",
                "Distillate": "distillate"
            }

        for n, v in stream_dict.items():
            port_obj = getattr(self, v)

            stream_attributes[n] = {}

            for k in port_obj.vars:
                for i in port_obj.vars[k].keys():
                    if isinstance(i, float):
                        stream_attributes[n][k] = value(
                            port_obj.vars[k][time_point])
                    else:
                        if len(i) == 2:
                            kname = str(i[1])
                        else:
                            kname = str(i[1:])
                        stream_attributes[n][k + " " + kname] = \
                            value(port_obj.vars[k][time_point, i[1:]])

        return DataFrame.from_dict(stream_attributes, orient="columns")
예제 #3
0
class PlateHeatExchangerData(HeatExchangerNTUData):
    """Plate Heat Exchanger(PHE) Unit Model."""

    CONFIG = HeatExchangerNTUData.CONFIG()

    CONFIG.declare(
        "passes",
        ConfigValue(
            default=4,
            domain=Integer,
            description="Number of passes",
            doc="""Number of passes of the fluids through the heat exchanger"""
        ))
    CONFIG.declare(
        "channels_per_pass",
        ConfigValue(
            default=12,
            domain=Integer,
            description="Number of channels for each pass",
            doc="""Number of channels to be used in each pass where a channel
               is the space between two plates with a flowing fluid"""))
    CONFIG.declare(
        "number_of_divider_plates",
        ConfigValue(
            default=0,
            domain=Integer,
            description="Number of divider plates in heat exchanger",
            doc="""Divider plates are used to create separate partitions in the
        unit. Each pass can be separated by a divider plate"""))

    # Update config block setting for pressure change to always be true
    CONFIG.hot_side.has_pressure_change = True
    CONFIG.hot_side.get("has_pressure_change").set_domain(In([True]))
    CONFIG.hot_side.get("has_pressure_change")._description = (
        "Pressure change term construction flag - must be True")
    CONFIG.hot_side.get("has_pressure_change")._doc = (
        "Plate Heat Exchanger model includes correlations for pressure drop "
        "thus has_pressure_change must be True")

    CONFIG.cold_side.has_pressure_change = True
    CONFIG.cold_side.get("has_pressure_change").set_domain(In([True]))
    CONFIG.cold_side.get("has_pressure_change")._description = (
        "Pressure change term construction flag - must be True")
    CONFIG.cold_side.get("has_pressure_change")._doc = (
        "Plate Heat Exchanger model includes correlations for pressure drop "
        "thus has_pressure_change must be True")

    def build(self):
        # Call super.build to setup model
        # This will create the control volumes, ports and basic equations
        super().build()

        # Units will be based on hot side properties
        units_meta = self.config.hot_side.property_package.get_metadata(
        ).get_derived_units

        # ---------------------------------------------------------------------
        # Plate design variables and parameter
        self.number_of_passes = Param(initialize=self.config.passes,
                                      units=pyunits.dimensionless,
                                      domain=PositiveIntegers,
                                      doc="Number of hot/cold fluid passes",
                                      mutable=True)

        # Assuming number of channels is equal in all plates
        self.channels_per_pass = Param(
            initialize=self.config.channels_per_pass,
            units=pyunits.dimensionless,
            domain=PositiveIntegers,
            doc="Number of channels in each pass",
            mutable=True)

        self.number_of_divider_plates = Param(
            initialize=self.config.number_of_divider_plates,
            units=pyunits.dimensionless,
            domain=NonNegativeIntegers,
            doc="Number of divider plates in heat exchanger",
            mutable=True)

        self.plate_length = Var(initialize=1.6925,
                                units=units_meta("length"),
                                domain=PositiveReals,
                                doc="Length of heat exchanger plate")
        self.plate_width = Var(initialize=0.6135,
                               units=units_meta("length"),
                               domain=PositiveReals,
                               doc="Width of heat exchanger plate")
        self.plate_thickness = Var(initialize=0.0006,
                                   units=units_meta("length"),
                                   domain=PositiveReals,
                                   doc="Thickness of heat exchanger plate")
        self.plate_pact_length = Var(initialize=0.381,
                                     units=units_meta("length"),
                                     domain=PositiveReals,
                                     doc="Compressed plate pact length")
        self.port_diameter = Var(initialize=0.2045,
                                 units=units_meta("length"),
                                 domain=PositiveReals,
                                 doc="Port diamter")

        self.plate_therm_cond = Var(
            initialize=16.2,
            units=units_meta("thermal_conductivity"),
            domain=PositiveReals,
            doc="Thermal conductivity heat exchanger plates")

        # Set default value of total heat transfer area
        self.area.set_value(114.3)

        # ---------------------------------------------------------------------
        # Derived geometric quantities
        total_plates = (2 * self.channels_per_pass * self.number_of_passes +
                        1 + self.number_of_divider_plates)
        total_active_plates = (
            2 * self.channels_per_pass * self.number_of_passes -
            (1 + self.number_of_divider_plates))

        self.plate_gap = Expression(
            expr=self.plate_pact_length / total_plates - self.plate_thickness)

        self.plate_area = Expression(expr=self.area / total_active_plates,
                                     doc='Heat transfer area of single plate')

        self.surface_enlargement_factor = Expression(
            expr=self.plate_area / (self.plate_length * self.plate_width))

        # Channel equivalent diameter
        self.channel_diameter = Expression(expr=2 * self.plate_gap /
                                           self.surface_enlargement_factor,
                                           doc="Channel equivalent diameter")

        # ---------------------------------------------------------------------
        # Fluid velocities
        def rule_port_vel_hot(blk, t):
            return (4 * blk.hot_side.properties_in[t].flow_vol /
                    (Constants.pi * blk.port_diameter**2))

        self.hot_port_velocity = Expression(self.flowsheet().time,
                                            rule=rule_port_vel_hot,
                                            doc='Hot side port velocity')

        def rule_port_vel_cold(blk, t):
            return (4 *
                    pyunits.convert(blk.cold_side.properties_in[t].flow_vol,
                                    to_units=units_meta("flow_vol")) /
                    (Constants.pi * blk.port_diameter**2))

        self.cold_port_velocity = Expression(self.flowsheet().time,
                                             rule=rule_port_vel_cold,
                                             doc='Cold side port velocity')

        def rule_channel_vel_hot(blk, t):
            return (blk.hot_side.properties_in[t].flow_vol /
                    (blk.channels_per_pass * blk.plate_width * blk.plate_gap))

        self.hot_channel_velocity = Expression(self.flowsheet().time,
                                               rule=rule_channel_vel_hot,
                                               doc='Hot side channel velocity')

        def rule_channel_vel_cold(blk, t):
            return (pyunits.convert(blk.cold_side.properties_in[t].flow_vol,
                                    to_units=units_meta("flow_vol")) /
                    (blk.channels_per_pass * blk.plate_width * blk.plate_gap))

        self.cold_channel_velocity = Expression(
            self.flowsheet().time,
            rule=rule_channel_vel_cold,
            doc='Cold side channel velocity')

        # ---------------------------------------------------------------------
        # Reynolds & Prandtl numbers
        # Density cancels out of Reynolds number if mass flow rate is used
        def rule_Re_h(blk, t):
            return (blk.hot_side.properties_in[t].flow_mass *
                    blk.channel_diameter /
                    (blk.channels_per_pass * blk.plate_width * blk.plate_gap *
                     blk.hot_side.properties_in[t].visc_d_phase["Liq"]))

        self.Re_hot = Expression(self.flowsheet().time,
                                 rule=rule_Re_h,
                                 doc='Hot side Reynolds number')

        def rule_Re_c(blk, t):
            return (pyunits.convert(
                blk.cold_side.properties_in[t].flow_mass /
                blk.cold_side.properties_in[t].visc_d_phase["Liq"],
                to_units=units_meta("length")) * blk.channel_diameter /
                    (blk.channels_per_pass * blk.plate_width * blk.plate_gap))

        self.Re_cold = Expression(self.flowsheet().time,
                                  rule=rule_Re_c,
                                  doc='Cold side Reynolds number')

        def rule_Pr_h(blk, t):
            return (blk.hot_side.properties_in[t].cp_mol /
                    blk.hot_side.properties_in[t].mw *
                    blk.hot_side.properties_in[t].visc_d_phase["Liq"] /
                    blk.hot_side.properties_in[t].therm_cond_phase["Liq"])

        self.Pr_hot = Expression(self.flowsheet().time,
                                 rule=rule_Pr_h,
                                 doc='Hot side Prandtl number')

        def rule_Pr_c(blk, t):
            return (blk.cold_side.properties_in[t].cp_mol /
                    blk.cold_side.properties_in[t].mw *
                    blk.cold_side.properties_in[t].visc_d_phase["Liq"] /
                    blk.cold_side.properties_in[t].therm_cond_phase["Liq"])

        self.Pr_cold = Expression(self.flowsheet().time,
                                  rule=rule_Pr_c,
                                  doc='Cold side Prandtl number')

        # ---------------------------------------------------------------------
        # Heat transfer coefficients
        # Parameters for Nusselt number correlation
        self.Nusselt_param_a = Param(initialize=0.4,
                                     domain=PositiveReals,
                                     units=pyunits.dimensionless,
                                     mutable=True,
                                     doc='Nusselt parameter A')
        self.Nusselt_param_b = Param(initialize=0.663,
                                     domain=PositiveReals,
                                     units=pyunits.dimensionless,
                                     mutable=True,
                                     doc='Nusselt parameter B')
        self.Nusselt_param_c = Param(initialize=0.333,
                                     domain=PositiveReals,
                                     units=pyunits.dimensionless,
                                     mutable=True,
                                     doc='Nusselt parameter C')

        # Film heat transfer coefficients
        def rule_hotside_transfer_coeff(blk, t):
            return (blk.hot_side.properties_in[t].therm_cond_phase["Liq"] /
                    blk.channel_diameter * blk.Nusselt_param_a *
                    blk.Re_hot[t]**blk.Nusselt_param_b *
                    blk.Pr_hot[t]**blk.Nusselt_param_c)

        self.heat_transfer_coefficient_hot_side = Expression(
            self.flowsheet().time,
            rule=rule_hotside_transfer_coeff,
            doc='Hot side heat transfer coefficient')

        def rule_coldside_transfer_coeff(blk, t):
            return (pyunits.convert(
                blk.cold_side.properties_in[t].therm_cond_phase["Liq"],
                to_units=units_meta("thermal_conductivity")) /
                    blk.channel_diameter * blk.Nusselt_param_a *
                    blk.Re_cold[t]**blk.Nusselt_param_b *
                    blk.Pr_cold[t]**blk.Nusselt_param_c)

        self.heat_transfer_coefficient_cold_side = Expression(
            self.flowsheet().time,
            rule=rule_coldside_transfer_coeff,
            doc='Cold side heat transfer coefficient')

        # Overall heat transfer coefficient
        def rule_U(blk, t):
            return blk.heat_transfer_coefficient[t] == (
                1.0 / (1.0 / blk.heat_transfer_coefficient_hot_side[t] +
                       blk.plate_gap / blk.plate_therm_cond +
                       1.0 / blk.heat_transfer_coefficient_cold_side[t]))

        self.overall_heat_transfer_eq = Constraint(
            self.flowsheet().time,
            rule=rule_U,
            doc='Calculations of overall heat transfer coefficient')

        # Effectiveness based on sub-heat exchangers
        # Divide NTU by number of channels per pass
        def rule_Ecf(blk, t):
            if blk.number_of_passes.value % 2 == 0:
                return (
                    blk.effectiveness[t] ==
                    (1 - exp(-blk.NTU[t] / blk.channels_per_pass *
                             (1 - blk.Cratio[t]))) /
                    (1 -
                     blk.Cratio[t] * exp(-blk.NTU[t] / blk.channels_per_pass *
                                         (1 - blk.Cratio[t]))))
            elif blk.pass_num.value % 2 == 1:
                return (blk.effectiveness[t] ==
                        (1 - exp(-blk.NTU[t] / blk.channels_per_pass *
                                 (1 + blk.Cratio[t]))) / (1 + blk.Cratio[t]))

        self.effectiveness_correlation = Constraint(
            self.flowsheet().time,
            rule=rule_Ecf,
            doc='Correlation for effectiveness factor')

        # ---------------------------------------------------------------------
        # Pressure drop correlations
        # Friction factor calculation
        self.friction_factor_param_a = Param(initialize=0.0,
                                             units=pyunits.dimensionless,
                                             doc='Friction factor parameter A',
                                             mutable=True)
        self.friction_factor_param_b = Param(initialize=18.29,
                                             units=pyunits.dimensionless,
                                             doc='Friction factor parameter B',
                                             mutable=True)
        self.friction_factor_param_c = Param(initialize=-0.652,
                                             units=pyunits.dimensionless,
                                             doc='Friction factor parameter C',
                                             mutable=True)

        def rule_fric_h(blk, t):
            return (blk.friction_factor_param_a + blk.friction_factor_param_b *
                    blk.Re_hot[t]**(blk.friction_factor_param_c))

        self.friction_factor_hot = Expression(self.flowsheet().time,
                                              rule=rule_fric_h,
                                              doc='Hot side friction factor')

        def rule_fric_c(blk, t):
            return (blk.friction_factor_param_a + blk.friction_factor_param_b *
                    blk.Re_cold[t]**(blk.friction_factor_param_c))

        self.friction_factor_cold = Expression(self.flowsheet().time,
                                               rule=rule_fric_c,
                                               doc='Cold side friction factor')

        def rule_hotside_dP(blk, t):
            return blk.hot_side.deltaP[t] == -(
                (2 * blk.friction_factor_hot[t] *
                 (blk.plate_length + blk.port_diameter) *
                 blk.number_of_passes * blk.hot_channel_velocity[t]**2 *
                 blk.hot_side.properties_in[t].dens_mass /
                 blk.channel_diameter) +
                (0.7 * blk.number_of_passes * blk.hot_port_velocity[t]**2 *
                 blk.hot_side.properties_in[t].dens_mass) +
                (blk.hot_side.properties_in[t].dens_mass *
                 pyunits.convert(Constants.acceleration_gravity,
                                 to_units=units_meta("acceleration")) *
                 (blk.plate_length + blk.port_diameter)))

        self.hot_side_deltaP_eq = Constraint(self.flowsheet().time,
                                             rule=rule_hotside_dP)

        def rule_coldside_dP(blk, t):
            return blk.cold_side.deltaP[t] == -(
                (2 * blk.friction_factor_cold[t] *
                 (blk.plate_length + blk.port_diameter) *
                 blk.number_of_passes * blk.cold_channel_velocity[t]**2 *
                 pyunits.convert(blk.cold_side.properties_in[t].dens_mass,
                                 to_units=units_meta("density_mass")) /
                 blk.channel_diameter) +
                (0.7 * blk.number_of_passes * blk.cold_port_velocity[t]**2 *
                 pyunits.convert(blk.cold_side.properties_in[t].dens_mass,
                                 to_units=units_meta("density_mass"))) +
                (pyunits.convert(blk.cold_side.properties_in[t].dens_mass,
                                 to_units=units_meta("density_mass")) *
                 pyunits.convert(Constants.acceleration_gravity,
                                 to_units=units_meta("acceleration")) *
                 (blk.plate_length + blk.port_diameter)))

        self.cold_side_deltaP_eq = Constraint(self.flowsheet().time,
                                              rule=rule_coldside_dP)

    def initialize(
        self,
        hot_side_state_args=None,
        cold_side_state_args=None,
        outlvl=idaeslog.NOTSET,
        solver=None,
        optarg=None,
        duty=None,
    ):
        """
        Heat exchanger initialization method.

        Args:
            hot_side_state_args : a dict of arguments to be passed to the
                property initialization for the hot side (see documentation of
                the specific property package) (default = None).
            cold_side_state_args : a dict of arguments to be passed to the
                property initialization for the cold side (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)
            duty : an initial guess for the amount of heat transfered. This
                should be a tuple in the form (value, units), (default
                = (1000 J/s))

        Returns:
            None

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

        hot_side = self.hot_side
        cold_side = self.cold_side

        # Create solver
        opt = get_solver(solver, optarg)

        flags1 = hot_side.initialize(outlvl=outlvl,
                                     optarg=optarg,
                                     solver=solver,
                                     state_args=hot_side_state_args)

        init_log.info_high("Initialization Step 1a (hot side) Complete.")

        flags2 = cold_side.initialize(outlvl=outlvl,
                                      optarg=optarg,
                                      solver=solver,
                                      state_args=cold_side_state_args)

        init_log.info_high("Initialization Step 1b (cold side) Complete.")

        # ---------------------------------------------------------------------
        # Solve unit without heat transfer equation
        # if costing block exists, deactivate
        if hasattr(self, "costing"):
            self.costing.deactivate()

        self.energy_balance_constraint.deactivate()
        self.effectiveness_correlation.deactivate()
        self.effectiveness.fix(0.68)

        # Get side 1 and side 2 heat units, and convert duty as needed
        s1_units = hot_side.heat.get_units()
        s2_units = cold_side.heat.get_units()

        if duty is None:
            # Assume 1000 J/s and check for unitless properties
            if s1_units is None and s2_units is None:
                # Backwards compatability for unitless properties
                s1_duty = -1000
                s2_duty = 1000
            else:
                s1_duty = pyunits.convert_value(-1000,
                                                from_units=pyunits.W,
                                                to_units=s1_units)
                s2_duty = pyunits.convert_value(1000,
                                                from_units=pyunits.W,
                                                to_units=s2_units)
        else:
            # Duty provided with explicit units
            s1_duty = -pyunits.convert_value(
                duty[0], from_units=duty[1], to_units=s1_units)
            s2_duty = pyunits.convert_value(duty[0],
                                            from_units=duty[1],
                                            to_units=s2_units)

        cold_side.heat.fix(s2_duty)
        for i in hot_side.heat:
            hot_side.heat[i].value = s1_duty

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)

        init_log.info_high("Initialization Step 2 {}.".format(
            idaeslog.condition(res)))

        cold_side.heat.unfix()
        self.energy_balance_constraint.activate()

        for t in self.effectiveness:
            calculate_variable_from_constraint(
                self.effectiveness[t], self.effectiveness_correlation[t])

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

        self.effectiveness_correlation.activate()
        self.effectiveness.unfix()

        # ---------------------------------------------------------------------
        # Final solve of full modelr
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)
        init_log.info_high("Initialization Step 4 {}.".format(
            idaeslog.condition(res)))

        # ---------------------------------------------------------------------
        # Release Inlet state
        hot_side.release_state(flags1, outlvl=outlvl)
        cold_side.release_state(flags2, outlvl=outlvl)

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

        # if costing block exists, activate and initialize
        if hasattr(self, "costing"):
            self.costing.activate()
            costing.initialize(self.costing)
예제 #4
0
class HeatExchangerNTUData(UnitModelBlockData):
    """Heat Exchanger Unit Model using NTU method."""

    CONFIG = UnitModelBlockData.CONFIG()

    # Configuration template for fluid specific  arguments
    _SideCONFIG = ConfigBlock()

    _SideCONFIG.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.}"""))
    _SideCONFIG.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.}"""))
    _SideCONFIG.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.}"""))
    _SideCONFIG.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.}"""))
    _SideCONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use ",
            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.}"""))
    _SideCONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property package",
            doc="""A ConfigBlock with arguments to be passed to
        property block(s) and used when constructing these,
        **default** - None.
        **Valid values:** {
        see property package for documentation.}"""))

    # Create individual config blocks for hot and cold sides
    CONFIG.declare("hot_side", _SideCONFIG(doc="Hot fluid config arguments"))
    CONFIG.declare("cold_side", _SideCONFIG(doc="Cold fluid config arguments"))

    def build(self):
        # Call UnitModel.build to setup model
        super().build()

        # ---------------------------------------------------------------------
        # Build hot-side control volume
        self.hot_side = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.hot_side.property_package,
                "property_package_args":
                self.config.hot_side.property_package_args
            })

        # TODO : Add support for phase equilibrium?
        self.hot_side.add_state_blocks(has_phase_equilibrium=False)

        self.hot_side.add_material_balances(
            balance_type=self.config.hot_side.material_balance_type,
            has_phase_equilibrium=False)

        self.hot_side.add_energy_balances(
            balance_type=self.config.hot_side.energy_balance_type,
            has_heat_transfer=True)

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

        # ---------------------------------------------------------------------
        # Build cold-side control volume
        self.cold_side = ControlVolume0DBlock(
            default={
                "dynamic":
                self.config.dynamic,
                "has_holdup":
                self.config.has_holdup,
                "property_package":
                self.config.cold_side.property_package,
                "property_package_args":
                self.config.cold_side.property_package_args
            })

        self.cold_side.add_state_blocks(has_phase_equilibrium=False)

        self.cold_side.add_material_balances(
            balance_type=self.config.cold_side.material_balance_type,
            has_phase_equilibrium=False)

        self.cold_side.add_energy_balances(
            balance_type=self.config.cold_side.energy_balance_type,
            has_heat_transfer=True)

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

        # ---------------------------------------------------------------------
        # Add Ports to control volumes
        self.add_inlet_port(name="hot_inlet",
                            block=self.hot_side,
                            doc='Hot side inlet port')
        self.add_outlet_port(name="hot_outlet",
                             block=self.hot_side,
                             doc='Hot side outlet port')

        self.add_inlet_port(name="cold_inlet",
                            block=self.cold_side,
                            doc='Cold side inlet port')
        self.add_outlet_port(name="cold_outlet",
                             block=self.cold_side,
                             doc='Cold side outlet port')

        # ---------------------------------------------------------------------
        # Add unit level References
        # Set references to balance terms at unit level
        self.heat_duty = Reference(self.cold_side.heat[:])

        # ---------------------------------------------------------------------
        # Add performance equations
        # All units of measurement will be based on hot side
        hunits = self.config.hot_side.property_package.get_metadata(
        ).get_derived_units

        # Common heat exchanger variables
        self.area = Var(initialize=1,
                        units=hunits("area"),
                        domain=PositiveReals,
                        doc="Heat transfer area")

        self.heat_transfer_coefficient = Var(
            self.flowsheet().time,
            initialize=1,
            units=hunits("heat_transfer_coefficient"),
            domain=PositiveReals,
            doc="Overall heat transfer coefficient")

        # Overall energy balance
        def rule_energy_balance(blk, t):
            return blk.hot_side.heat[t] == -pyunits.convert(
                blk.cold_side.heat[t], to_units=hunits("power"))

        self.energy_balance_constraint = Constraint(self.flowsheet().time,
                                                    rule=rule_energy_balance)

        # Add e-NTU variables
        self.effectiveness = Var(self.flowsheet().time,
                                 initialize=1,
                                 units=pyunits.dimensionless,
                                 domain=PositiveReals,
                                 doc="Effectiveness factor for NTU method")

        # Minimum heat capacitance ratio for e-NTU method
        self.eps_cmin = Param(initialize=1e-3,
                              mutable=True,
                              units=hunits("power") / hunits("temperature"),
                              doc="Epsilon parameter for smooth Cmin and Cmax")

        # TODO : Support both mass and mole based flows
        def rule_Cmin(blk, t):
            caph = (blk.hot_side.properties_in[t].flow_mol *
                    blk.hot_side.properties_in[t].cp_mol)
            capc = pyunits.convert(blk.cold_side.properties_in[t].flow_mol *
                                   blk.cold_side.properties_in[t].cp_mol,
                                   to_units=hunits("power") /
                                   hunits("temperature"))
            return smooth_min(caph, capc, eps=blk.eps_cmin)

        self.Cmin = Expression(self.flowsheet().time,
                               rule=rule_Cmin,
                               doc='Minimum heat capacitance rate')

        def rule_Cmax(blk, t):
            caph = (blk.hot_side.properties_in[t].flow_mol *
                    blk.hot_side.properties_in[t].cp_mol)
            capc = pyunits.convert(blk.cold_side.properties_in[t].flow_mol *
                                   blk.cold_side.properties_in[t].cp_mol,
                                   to_units=hunits("power") /
                                   hunits("temperature"))
            return smooth_max(caph, capc, eps=blk.eps_cmin)

        self.Cmax = Expression(self.flowsheet().time,
                               rule=rule_Cmax,
                               doc='Maximum heat capacitance rate')

        # Heat capacitance ratio
        def rule_Cratio(blk, t):
            return blk.Cmin[t] / blk.Cmax[t]

        self.Cratio = Expression(self.flowsheet().time,
                                 rule=rule_Cratio,
                                 doc='Heat capacitance ratio')

        def rule_NTU(blk, t):
            return blk.heat_transfer_coefficient[t] * blk.area / blk.Cmin[t]

        self.NTU = Expression(self.flowsheet().time,
                              rule=rule_NTU,
                              doc='Number of heat transfer units')

        # Heat transfer by e-NTU method
        def rule_entu(blk, t):
            return blk.hot_side.heat[t] == -(
                blk.effectiveness[t] * blk.Cmin[t] *
                (blk.hot_side.properties_in[t].temperature -
                 pyunits.convert(blk.cold_side.properties_in[t].temperature,
                                 to_units=hunits("temperature"))))

        self.heat_duty_constraint = Constraint(self.flowsheet().time,
                                               rule=rule_entu)

    # TODO : Add scaling methods

    def initialize(
        self,
        hot_side_state_args=None,
        cold_side_state_args=None,
        outlvl=idaeslog.NOTSET,
        solver=None,
        optarg=None,
        duty=None,
    ):
        """
        Heat exchanger initialization method.

        Args:
            hot_side_state_args : a dict of arguments to be passed to the
                property initialization for the hot side (see documentation of
                the specific property package) (default = None).
            cold_side_state_args : a dict of arguments to be passed to the
                property initialization for the cold side (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)
            duty : an initial guess for the amount of heat transfered. This
                should be a tuple in the form (value, units), (default
                = (1000 J/s))

        Returns:
            None

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

        hot_side = self.hot_side
        cold_side = self.cold_side

        # Create solver
        opt = get_solver(solver, optarg)

        flags1 = hot_side.initialize(outlvl=outlvl,
                                     optarg=optarg,
                                     solver=solver,
                                     state_args=hot_side_state_args)

        init_log.info_high("Initialization Step 1a (hot side) Complete.")

        flags2 = cold_side.initialize(outlvl=outlvl,
                                      optarg=optarg,
                                      solver=solver,
                                      state_args=cold_side_state_args)

        init_log.info_high("Initialization Step 1b (cold side) Complete.")

        # ---------------------------------------------------------------------
        # Solve unit without heat transfer equation
        # if costing block exists, deactivate
        if hasattr(self, "costing"):
            self.costing.deactivate()

        self.energy_balance_constraint.deactivate()

        # Get side 1 and side 2 heat units, and convert duty as needed
        s1_units = hot_side.heat.get_units()
        s2_units = cold_side.heat.get_units()

        if duty is None:
            # Assume 1000 J/s and check for unitless properties
            if s1_units is None and s2_units is None:
                # Backwards compatability for unitless properties
                s1_duty = -1000
                s2_duty = 1000
            else:
                s1_duty = pyunits.convert_value(-1000,
                                                from_units=pyunits.W,
                                                to_units=s1_units)
                s2_duty = pyunits.convert_value(1000,
                                                from_units=pyunits.W,
                                                to_units=s2_units)
        else:
            # Duty provided with explicit units
            s1_duty = -pyunits.convert_value(
                duty[0], from_units=duty[1], to_units=s1_units)
            s2_duty = pyunits.convert_value(duty[0],
                                            from_units=duty[1],
                                            to_units=s2_units)

        cold_side.heat.fix(s2_duty)
        for i in hot_side.heat:
            hot_side.heat[i].value = s1_duty

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(self, tee=slc.tee)

        init_log.info_high("Initialization Step 2 {}.".format(
            idaeslog.condition(res)))

        cold_side.heat.unfix()
        self.energy_balance_constraint.activate()

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

        # ---------------------------------------------------------------------
        # Release Inlet state
        hot_side.release_state(flags1, outlvl=outlvl)
        cold_side.release_state(flags2, outlvl=outlvl)

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

        # if costing block exists, activate and initialize
        if hasattr(self, "costing"):
            self.costing.activate()
            costing.initialize(self.costing)

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(
            {
                "Hot Inlet": self.hot_inlet,
                "Hot Outlet": self.hot_outlet,
                "Cold Inlet": self.cold_inlet,
                "Cold Outlet": self.cold_outlet,
            },
            time_point=time_point,
        )

    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.hx_costing(self.costing, **kwargs)