コード例 #1
0
ファイル: balance.py プロジェクト: sodanetworks/idaes-pse
class BalanceBlockData(UnitModelBlockData):
    """
    Simple mass and energy balance unit.
    """
    CONFIG = UnitModelBlockData.CONFIG()
    make_balance_config_block(CONFIG)

    def build(self):
        """Building model

        Args:
            None
        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super().build()
        # Add Control Volume
        make_balance_control_volume(self, "control_volume", self.config)
        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()
        # Add convienient references to control volume quantities deltaP.
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            self.deltaP = Reference(self.control_volume.deltaP)
        if self.config.has_heat_transfer is True:
            self.heat_duty = Reference(self.control_volume.heat)
        if self.config.has_work_transfer is True:
            self.work = Reference(self.control_volume.work)
コード例 #2
0
ファイル: heater.py プロジェクト: milesabarr/idaes-pse
class HeaterData(UnitModelBlockData):
    """
    Simple 0D heater unit.
    Unit model to add or remove heat from a material.
    """
    CONFIG = UnitModelBlockData.CONFIG()
    _make_heater_config_block(CONFIG)

    def build(self):
        """Building model

        Args:
            None
        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(HeaterData, self).build()
        # Add Control Volume
        _make_heater_control_volume(self, "control_volume", self.config)
        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()
        # Add a convienient reference to heat duty.
        self.heat_duty = Reference(self.control_volume.heat)
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):

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

    def _get_performance_contents(self, time_point=0):
        return {"vars": {"Heat Duty": self.heat_duty[time_point]}}
コード例 #3
0
class _UnitData(UnitModelBlockData):
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare("property_package", ConfigValue(default=None))
    CONFIG.declare("property_package_args", ConfigValue(default={}))

    def build(self):
        super(_UnitData, self).build()
コード例 #4
0
class HeaterData(UnitModelBlockData):
    """
    Simple 0D heater unit.
    Unit model to add or remove heat from a material.
    """
    CONFIG = UnitModelBlockData.CONFIG()
    _make_heater_config_block(CONFIG)

    def build(self):
        """
        Building model
        Args:
            None
        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(HeaterData, self).build()
        # Add Control Volume
        _make_heater_control_volume(self, "control_volume", self.config)
        # Add Ports
        self.add_inlet_port()
        self.add_outlet_port()
        # Add a convienient reference to heat duty.
        add_object_reference(self, "heat_duty", self.control_volume.heat)
コード例 #5
0
class StoichiometricReactorData(UnitModelBlockData):
    """
    Standard Stoichiometric Reactor Unit Model Class
    This model assumes that all given reactions are irreversible, and that each
    reaction has a fixed rate_reaction extent which has to be specified by the
    user.
    """
    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_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.}"""))

    def build(self):
        """
        Begin building model (pre-DAE transformation).
        Args:
            None
        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(StoichiometricReactorData, 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,
                "reaction_package": self.config.reaction_package,
                "reaction_package_args": self.config.reaction_package_args
            })

        self.control_volume.add_state_blocks(has_phase_equilibrium=False)

        self.control_volume.add_reaction_blocks(has_equilibrium=False)

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

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

        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
        self.rate_reaction_extent = Reference(
            self.control_volume.rate_reaction_extent[...])

        # 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[:])
        if (self.config.has_pressure_change is True and
                self.config.momentum_balance_type != MomentumBalanceType.none):
            self.deltaP = Reference(self.control_volume.deltaP[:])

    def _get_performance_contents(self, time_point=0):
        var_dict = {}
        for r in self.config.reaction_package.rate_reaction_idx:
            var_dict[f"Reaction Extent [{r}]"] = \
                self.rate_reaction_extent[time_point, r]
        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}
コード例 #6
0
class HeatExchangerWith3StreamsData(UnitModelBlockData):
    """
    Standard Heat Exchanger Unit Model Class
    """
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare(
        "side_1_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(
        "side_1_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(
        "side_2_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(
        "side_2_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(
        "side_3_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(
        "side_3_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(
        "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=True,
            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(
        "flow_type_side_2",
        ConfigValue(
            default='counter-current',
            domain=In(['counter-current', 'co-current']),
            description="Flow configuration in unit",
            doc="""Flag indicating type of flow arrangement to use for heat
exchanger, **default** 'counter-current' counter-current flow arrangement"""))
    CONFIG.declare(
        "flow_type_side_3",
        ConfigValue(
            default='counter-current',
            domain=In(['counter-current', 'co-current']),
            description="Flow configuration in unit",
            doc="""Flag indicating type of flow arrangement to use for heat
exchanger (default = 'counter-current' - counter-current flow arrangement"""))

    def build(self):
        """
        Begin building model
        """
        # Call UnitModel.build to setup dynamics
        super(HeatExchangerWith3StreamsData, self).build()

        # Build Holdup Block
        self.side_1 = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.side_1_property_package,
                "property_package_args":
                self.config.side_1_property_package_args
            })

        self.side_2 = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.side_2_property_package,
                "property_package_args":
                self.config.side_2_property_package_args
            })

        self.side_3 = ControlVolume0DBlock(
            default={
                "dynamic": self.config.dynamic,
                "has_holdup": self.config.has_holdup,
                "property_package": self.config.side_3_property_package,
                "property_package_args":
                self.config.side_3_property_package_args
            })

        # Add Geometry
        self.side_1.add_geometry()
        self.side_2.add_geometry()
        self.side_3.add_geometry()

        # Add state block
        self.side_1.add_state_blocks(has_phase_equilibrium=False)

        # Add material balance
        self.side_1.add_material_balances(
            balance_type=self.config.material_balance_type)
        # add energy balance
        self.side_1.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_transfer=self.config.has_heat_transfer)
        # add momentum balance
        self.side_1.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # Add state block
        self.side_2.add_state_blocks(has_phase_equilibrium=False)

        # Add material balance
        self.side_2.add_material_balances(
            balance_type=self.config.material_balance_type)
        # add energy balance
        self.side_2.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_transfer=self.config.has_heat_transfer)
        # add momentum balance
        self.side_2.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        # Add state block
        self.side_3.add_state_blocks(has_phase_equilibrium=False)

        # Add material balance
        self.side_3.add_material_balances(
            balance_type=self.config.material_balance_type)
        # add energy balance
        self.side_3.add_energy_balances(
            balance_type=self.config.energy_balance_type,
            has_heat_transfer=self.config.has_heat_transfer)
        # add momentum balance
        self.side_3.add_momentum_balances(
            balance_type=self.config.momentum_balance_type,
            has_pressure_change=self.config.has_pressure_change)

        self._set_geometry()

        # Construct performance equations
        self._make_performance()

        # Construct performance equations
        if self.config.flow_type_side_2 == "counter-current":
            self._make_counter_current_side_2()
        else:
            self._make_co_current_side_2()

        # Construct performance equations
        if self.config.flow_type_side_3 == "counter-current":
            self._make_counter_current_side_3()
        else:
            self._make_co_current_side_3()

        self.add_inlet_port(name="side_1_inlet", block=self.side_1)
        self.add_inlet_port(name="side_2_inlet", block=self.side_2)
        self.add_inlet_port(name="side_3_inlet", block=self.side_3)
        self.add_outlet_port(name="side_1_outlet", block=self.side_1)
        self.add_outlet_port(name="side_2_outlet", block=self.side_2)
        self.add_outlet_port(name="side_3_outlet", block=self.side_3)

    def _set_geometry(self):
        """
        Define the geometry of the unit as necessary, and link to holdup volume

        """

        # UA (product of overall heat transfer coefficient and area)
        # between side 1 and side 2
        self.ua_side_2 = Var(self.flowsheet().config.time,
                             initialize=10.0,
                             doc='UA between side 1 and side 2')

        # UA (product of overall heat transfer coefficient and area)
        # between side 1 and side 3
        self.ua_side_3 = Var(self.flowsheet().config.time,
                             initialize=10.0,
                             doc='UA between side 1 and side 3')

        # fraction of heat from hot stream as heat loss to ambient
        self.frac_heatloss = Var(initialize=0.05,
                                 doc='Fraction of heat loss to ambient')

        if self.config.has_holdup is True:
            self.volume_side_1 = Reference(self.side_1.volume)
            self.volume_side_2 = Reference(self.side_2.volume)
            self.volume_side_3 = Reference(self.side_3.volume)

    def _make_performance(self):
        """
        Define constraints which describe the behaviour of the unit model.

        Args:
            None

        Returns:
            None
        """
        # Set references to balance terms at unit level
        self.heat_duty_side_1 = Reference(self.side_1.heat)
        self.heat_duty_side_2 = Reference(self.side_2.heat)
        self.heat_duty_side_3 = Reference(self.side_3.heat)

        if self.config.has_pressure_change is True:
            self.deltaP_side_1 = Reference(self.side_1.deltaP)
            self.deltaP_side_2 = Reference(self.side_2.deltaP)
            self.deltaP_side_3 = Reference(self.side_3.deltaP)

        # Performance parameters and variables
        # Temperature driving force
        self.temperature_driving_force_side_2 = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Mean driving force '
            'for heat exchange')

        # Temperature driving force
        self.temperature_driving_force_side_3 = Var(
            self.flowsheet().config.time,
            initialize=1.0,
            doc='Mean driving force '
            'for heat exchange')

        # Temperature difference at side 2 inlet
        self.side_2_inlet_dT = Var(self.flowsheet().config.time,
                                   initialize=1.0,
                                   doc='Temperature difference '
                                   'at side 2 inlet')

        # Temperature difference at side 2 outlet
        self.side_2_outlet_dT = Var(self.flowsheet().config.time,
                                    initialize=1.0,
                                    doc='Temperature difference '
                                    'at side 2 outlet')

        # Temperature difference at side 3 inlet
        self.side_3_inlet_dT = Var(self.flowsheet().config.time,
                                   initialize=1.0,
                                   doc='Temperature difference'
                                   ' at side 3 inlet')

        # Temperature difference at side 3 outlet
        self.side_3_outlet_dT = Var(self.flowsheet().config.time,
                                    initialize=1.0,
                                    doc='Temperature difference '
                                    'at side 3 outlet')

        # Driving force side 2 (Underwood approximation)
        @self.Constraint(self.flowsheet().config.time,
                         doc="Log mean temperature difference calculation "
                         "using Underwood approximation")
        def LMTD_side_2(b, t):
            return b.temperature_driving_force_side_2[t] == \
                ((b.side_2_inlet_dT[t]**(1/3)
                  + b.side_2_outlet_dT[t]**(1/3))/2)**(3)

        # Driving force side 3 (Underwood approximation)
        @self.Constraint(self.flowsheet().config.time,
                         doc="Log mean temperature difference calculation "
                         "using Underwood approximation")
        def LMTD_side_3(b, t):
            return b.temperature_driving_force_side_3[t] == \
                ((b.side_3_inlet_dT[t]**(1/3)
                  + b.side_3_outlet_dT[t]**(1/3))/2)**(3)

        # Heat duty side 2
        @self.Constraint(self.flowsheet().config.time,
                         doc="Heat transfer rate")
        def heat_duty_side_2_eqn(b, t):
            return b.heat_duty_side_2[t] == \
                (b.ua_side_2[t] * b.temperature_driving_force_side_2[t])

        # Heat duty side 3
        @self.Constraint(self.flowsheet().config.time,
                         doc="Heat transfer rate")
        def heat_duty_side_3_eqn(b, t):
            return b.heat_duty_side_3[t] == \
                (b.ua_side_3[t]*b.temperature_driving_force_side_3[t])

        # Energy balance equation
        @self.Constraint(self.flowsheet().config.time,
                         doc="Energy balance between two sides")
        def heat_duty_side_1_eqn(b, t):
            return -b.heat_duty_side_1[t]*(1-b.frac_heatloss) == \
                (b.heat_duty_side_2[t] + b.heat_duty_side_3[t])

    def _make_co_current_side_2(self):
        """
        Add temperature driving force Constraints for co-current flow.


        """
        # Temperature Differences
        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 2 inlet temperature difference")
        def side_2_inlet_dT_eqn(b, t):
            return b.side_2_inlet_dT[t] == (
                b.side_1.properties_in[t].temperature -
                b.side_2.properties_in[t].temperature)

        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 2 outlet temperature difference")
        def side_2_outlet_dT_eqn(b, t):
            return b.side_2_outlet_dT[t] == (
                b.side_1.properties_out[t].temperature -
                b.side_2.properties_out[t].temperature)

    def _make_counter_current_side_2(self):
        """
        Add temperature driving force Constraints for counter-current flow.
        """
        # Temperature Differences
        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 2 inlet temperature difference")
        def side_2_inlet_dT_eqn(b, t):
            return b.side_2_inlet_dT[t] == (
                b.side_1.properties_out[t].temperature -
                b.side_2.properties_in[t].temperature)

        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 2 outlet temperature difference")
        def side_2_outlet_dT_eqn(b, t):
            return b.side_2_outlet_dT[t] == (
                b.side_1.properties_in[t].temperature -
                b.side_2.properties_out[t].temperature)

    def _make_co_current_side_3(self):
        """
        Add temperature driving force Constraints for co-current flow.
        """
        # Temperature Differences
        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 3 inlet temperature difference")
        def side_3_inlet_dT_eqn(b, t):
            return b.side_3_inlet_dT[t] == (
                b.side_1.properties_in[t].temperature -
                b.side_3.properties_in[t].temperature)

        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 3 outlet temperature difference")
        def side_3_outlet_dT_eqn(b, t):
            return b.side_3_outlet_dT[t] == (
                b.side_1.properties_out[t].temperature -
                b.side_3.properties_out[t].temperature)

    def _make_counter_current_side_3(self):
        """
        Add temperature driving force Constraints for counter-current flow.
        """
        # Temperature Differences
        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 3 inlet temperature difference")
        def side_3_inlet_dT_eqn(b, t):
            return b.side_3_inlet_dT[t] == (
                b.side_1.properties_out[t].temperature -
                b.side_3.properties_in[t].temperature)

        @self.Constraint(self.flowsheet().config.time,
                         doc="Side 3 outlet temperature difference")
        def side_3_outlet_dT_eqn(b, t):
            return b.side_3_outlet_dT[t] == (
                b.side_1.properties_in[t].temperature -
                b.side_3.properties_out[t].temperature)

    def initialize(blk,
                   state_args_1=None,
                   state_args_2=None,
                   state_args_3=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        '''
        General Heat Exchanger initialisation routine.

        Keyword Arguments:
            state_args_1 : a dict of arguments to be passed to the property
                           package(s) for side 1 of the heat exchanger to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            state_args_2 : a dict of arguments to be passed to the property
                           package(s) for side 2 of the heat exchanger to
                           provide an initial state for initialization
                           (see documentation of the specific property package)
                           (default = None).
            state_args_3 : a dict of arguments to be passed to the property
                           package(s) for side 3 of the heat exchanger 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)

        # ---------------------------------------------------------------------
        # Initialize inlet property blocks
        flags1 = blk.side_1.initialize(outlvl=outlvl,
                                       optarg=optarg,
                                       solver=solver,
                                       state_args=state_args_1)

        flags2 = blk.side_2.initialize(outlvl=outlvl,
                                       optarg=optarg,
                                       solver=solver,
                                       state_args=state_args_2)

        flags3 = blk.side_3.initialize(outlvl=outlvl,
                                       optarg=optarg,
                                       solver=solver,
                                       state_args=state_args_3)

        init_log.info('Initialisation Step 1 Complete.')

        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info("Initialization Step 2 Complete: {}".format(
            idaeslog.condition(res)))
        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.side_1.release_state(flags1, outlvl)
        blk.side_2.release_state(flags2, outlvl)
        blk.side_3.release_state(flags3, outlvl)

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

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

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

        for t, c in self.heat_duty_side_3_eqn.items():
            sf = iscale.get_scaling_factor(self.heat_duty_side_3[t],
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, sf, overwrite=False)
コード例 #7
0
class PHEData(UnitModelBlockData):
    """Plate Heat Exchanger(PHE) Unit Model."""

    CONFIG = UnitModelBlockData.CONFIG()

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

    CONFIG.declare(
        "passes",
        ConfigValue(
            default=4,
            domain=int,
            description="Number of passes",
            doc="""Number of passes of the fluids through the heat exchanger"""
        ))

    CONFIG.declare(
        "channel_list",
        ConfigValue(
            default=[12, 12, 12, 12],
            domain=list,
            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(
        "divider_plate_number",
        ConfigValue(
            default=0,
            domain=int,
            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"""))

    CONFIG.declare(
        "port_diameter",
        ConfigValue(
            default=0.2045,
            domain=float,
            description="Diameter of the ports on the plate [m]",
            doc="""Diameter of the ports on the plate for fluid entry/exit
               into a channel"""))

    CONFIG.declare(
        "plate_thermal_cond",
        ConfigValue(
            default=16.2,
            domain=float,
            description="Thermal conductivity [W/m.K]",
            doc="""Thermal conductivity of the plate material [W/m.K]"""))

    CONFIG.declare(
        "total_area",
        ConfigValue(
            default=114.3,
            domain=float,
            description="Total heat transfer area [m2]",
            doc="""Total heat transfer area as specifed by the manufacturer""")
    )

    CONFIG.declare(
        "plate_thickness",
        ConfigValue(default=0.0006,
                    domain=float,
                    description="Plate thickness [m]",
                    doc="""Plate thickness"""))

    CONFIG.declare(
        "plate_vertical_dist",
        ConfigValue(
            default=1.897,
            domain=float,
            description="Vertical distance between centers of ports [m].",
            doc=
            """Vertical distance between centers of ports.(Top and bottom ports)
            (approximately equals to the plate length)"""))

    CONFIG.declare(
        "plate_horizontal_dist",
        ConfigValue(
            default=0.409,
            domain=float,
            description="Horizontal distance between centers of ports [m].",
            doc=
            """Horizontal distance between centers of ports(Left and right ports)"""
        ))

    CONFIG.declare(
        "plate_pact_length",
        ConfigValue(default=0.381,
                    domain=float,
                    description="Compressed plate pact length [m].",
                    doc="""Compressed plate pact length.
               Length between the Head and the Follower"""))

    CONFIG.declare(
        "surface_enlargement_factor",
        ConfigValue(
            default=None,
            domain=float,
            description="Surface enlargement factor",
            doc="""Surface enlargement factor is the ratio of single plate area
               (obtained from the total area) to the projected plate area"""))

    CONFIG.declare(
        "plate_gap",
        ConfigValue(
            default=None,
            domain=float,
            description="Mean channel spacing or gap bewteen two plates [m]",
            doc="""The plate gap is the distance between two adjacent plates that
               forms a flow channel """))

    _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(PHEData, self).build()

        # Consistency check for number of passes and channels in each pass
        for i in self.config.channel_list:
            if not isinstance(i, int):
                raise ConfigurationError("number of channels ({}) must be"
                                         " an integer".format(i))

        if (self.config.passes != len(self.config.channel_list)):
            raise ConfigurationError(
                "The number of elements in the channel list: {}  "
                " does not match the number of passes ({}) given. "
                "Please provide as integers, the number of channels of each pass"
                .format(self.config.channel_list, self.config.passes))

        # ======================================================================
        # Build hot-side  Control Volume (Lean Solvent)
        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
            })

        self.hot_side.add_state_blocks(has_phase_equilibrium=False)

        self.hot_side.add_material_balances(
            balance_type=MaterialBalanceType.componentTotal,
            has_mass_transfer=False,
            has_phase_equilibrium=False,
            has_rate_reactions=False)

        self.hot_side.add_momentum_balances(
            balance_type=MomentumBalanceType.pressureTotal,
            has_pressure_change=True)

        # Energy balance is based on the effectiveness Number of Transfer units
        # (E-NTU method) and inluded as performance equations. Hence the control
        #  volume energy balances are not added.

        # ======================================================================
        # Build cold-side  Control Volume(Rich solvent)
        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=MaterialBalanceType.componentTotal,
            has_mass_transfer=False,
            has_phase_equilibrium=False,
            has_rate_reactions=False)

        self.cold_side.add_momentum_balances(
            balance_type=MomentumBalanceType.pressureTotal,
            has_pressure_change=True)

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

        # cold-side
        self.add_inlet_port(name="cold_inlet",
                            block=self.cold_side,
                            doc='inlet Port')
        self.add_outlet_port(name="cold_outlet",
                             block=self.cold_side,
                             doc='outlet Port')
        # ======================================================================
        # Add performace equation method
        self._make_params()
        self._make_performance_method()

    def _make_params(self):
        self.P = Param(initialize=self.config.passes,
                       units=None,
                       doc="Total number of passes for hot or cold fluid")
        self.PH = RangeSet(self.P, doc="Set of hot fluid passes")
        self.PC = RangeSet(self.P, doc="Set of cold fluid passes(equal to PH)")

        self.plate_thermal_cond = Param(
            mutable=True,
            initialize=self.config.plate_thermal_cond,
            units=pyunits.W / pyunits.m / pyunits.K,
            doc="Plate thermal conductivity")
        self.plate_thick = Param(mutable=True,
                                 initialize=self.config.plate_thickness,
                                 units=pyunits.m,
                                 doc="Plate thickness")

        self.port_dia = Param(mutable=True,
                              initialize=self.config.port_diameter,
                              units=pyunits.m,
                              doc=" Port diameter of plate ")
        self.Np = Param(self.PH,
                        units=None,
                        doc="Number of channels in each pass",
                        mutable=True)
        # Number of channels in each pass
        for i in self.PH:
            self.Np[i].value = self.config.channel_list[i - 1]

        # ---------------------------------------------------------------------
        # Assign plate specifications

        # effective plate length & width
        _effective_plate_length = self.config.plate_vertical_dist - \
            self.config.port_diameter
        _effective_plate_width = self.config.plate_horizontal_dist + \
            self.config.port_diameter

        self.plate_length = Expression(expr=_effective_plate_length)
        self.plate_width = Expression(expr=_effective_plate_width)

        # Area of single plate
        _total_active_plate_number = 2 * sum(self.config.channel_list) - 1 -\
            self.config.divider_plate_number

        self.plate_area = Expression(expr=self.config.total_area /
                                     _total_active_plate_number,
                                     doc="Heat transfer area of single plate")

        # Plate gap
        if self.config.plate_gap is None:
            _total_plate_number = 2 * sum(self.config.channel_list) + 1 +\
                self.config.divider_plate_number
            _plate_pitch = self.config.plate_pact_length / _total_plate_number

            _plate_gap = _plate_pitch - self.config.plate_thickness
        else:
            _plate_gap = self.config.plate_gap

        self.plate_gap = Expression(expr=_plate_gap)

        # Surface enlargement factor
        if self.config.surface_enlargement_factor is None:
            _projected_plate_area = _effective_plate_length * _effective_plate_width
            _surface_enlargement_factor = self.plate_area / _projected_plate_area
        else:
            _surface_enlargement_factor = self.config.surface_enlargement_factor

        self.surface_enlargement_factor = Expression(
            expr=_surface_enlargement_factor)

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

        # heat transfer parameters
        self.param_a = Var(initialize=0.3,
                           bounds=(0.2, 0.4),
                           units=None,
                           doc='Nusselt parameter')
        self.param_b = Var(initialize=0.663,
                           bounds=(0.3, 0.7),
                           units=None,
                           doc='Nusselt parameter')
        self.param_c = Var(initialize=1 / 3.0,
                           bounds=(1e-5, 2),
                           units=None,
                           doc='Nusselt parameter')
        self.param_a.fix(0.4)
        self.param_b.fix(0.663)
        self.param_c.fix(0.333)

    def _make_performance_method(self):

        solvent_list = self.config.hot_side.property_package.component_list_solvent

        def rule_trh(blk, t):
            return (blk.hot_side.properties_out[t].temperature /
                    blk.hot_side.properties_in[t].temperature)

        self.trh = Expression(self.flowsheet().config.time,
                              rule=rule_trh,
                              doc='Ratio of hot outlet temperature to hot'
                              'inlet temperature')

        def rule_trc(blk, t):
            return (blk.cold_side.properties_out[t].temperature /
                    blk.cold_side.properties_in[t].temperature)

        self.trc = Expression(self.flowsheet().config.time,
                              rule=rule_trc,
                              doc='Ratio of cold outlet temperature to cold'
                              ' inlet temperature')

        def rule_cp_comp_hot(blk, t, j):
            return 1e3 * (
                blk.hot_side.properties_in[t]._params.cp_param[j, 1] +
                blk.hot_side.properties_in[t]._params.cp_param[j, 2] / 2 *
                blk.hot_side.properties_in[t].temperature * (blk.trh[t] + 1) +
                blk.hot_side.properties_in[t]._params.cp_param[j, 3] / 3 *
                (blk.hot_side.properties_in[t].temperature**2) *
                (blk.trh[t]**2 + blk.trh[t] + 1) +
                blk.hot_side.properties_in[t]._params.cp_param[j, 4] / 4 *
                (blk.hot_side.properties_in[t].temperature**3) *
                (blk.trh[t] + 1) * (blk.trh[t]**2 + 1) +
                blk.hot_side.properties_in[t]._params.cp_param[j, 5] / 5 *
                (blk.hot_side.properties_in[t].temperature**4) *
                (blk.trh[t]**4 + blk.trh[t]**3 + blk.trh[t]**2 + blk.trh[t] +
                 1))

        self.cp_comp_hot = Expression(
            self.flowsheet().config.time,
            solvent_list,
            rule=rule_cp_comp_hot,
            doc='Component mean specific heat capacity'
            ' btw inlet and outlet'
            ' of hot-side temperature')

        def rule_cp_hot(blk, t):
            return sum(blk.cp_comp_hot[t, j] *
                       blk.hot_side.properties_in[t].mass_frac_co2_free[j]
                       for j in solvent_list)

        self.cp_hot = Expression(self.flowsheet().config.time,
                                 rule=rule_cp_hot,
                                 doc='Hot-side mean specific heat capacity on'
                                 'free CO2 basis')

        def rule_cp_comp_cold(blk, t, j):
            return 1e3 * (
                blk.cold_side.properties_in[t]._params.cp_param[j, 1] +
                blk.cold_side.properties_in[t]._params.cp_param[j, 2] / 2 *
                blk.cold_side.properties_in[t].temperature * (blk.trc[t] + 1) +
                blk.cold_side.properties_in[t]._params.cp_param[j, 3] / 3 *
                (blk.cold_side.properties_in[t].temperature**2) *
                (blk.trc[t]**2 + blk.trc[t] + 1) +
                blk.cold_side.properties_in[t]._params.cp_param[j, 4] / 4 *
                (blk.cold_side.properties_in[t].temperature**3) *
                (blk.trc[t] + 1) * (blk.trc[t]**2 + 1) +
                blk.cold_side.properties_in[t]._params.cp_param[j, 5] / 5 *
                (blk.cold_side.properties_in[t].temperature**4) *
                (blk.trc[t]**4 + blk.trc[t]**3 + blk.trc[t]**2 + blk.trc[t] +
                 1))

        self.cp_comp_cold = Expression(
            self.flowsheet().config.time,
            solvent_list,
            rule=rule_cp_comp_cold,
            doc='Component mean specific heat capacity'
            'btw inlet and outlet'
            ' of cold-side temperature')

        def rule_cp_cold(blk, t):
            return sum(blk.cp_comp_cold[t, j] *
                       blk.cold_side.properties_in[t].mass_frac_co2_free[j]
                       for j in solvent_list)

        self.cp_cold = Expression(self.flowsheet().config.time,
                                  rule=rule_cp_cold,
                                  doc='Cold-side mean specific heat capacity'
                                  'on free CO2 basis')

        # Model Variables
        self.Th_in = Var(self.flowsheet().config.time,
                         self.PH,
                         initialize=393,
                         units=pyunits.K,
                         doc="Hot Temperature IN of pass")
        self.Th_out = Var(self.flowsheet().config.time,
                          self.PH,
                          initialize=325,
                          units=pyunits.K,
                          doc="Hot Temperature OUT of pass")
        self.Tc_in = Var(self.flowsheet().config.time,
                         self.PH,
                         initialize=320,
                         units=pyunits.K,
                         doc="Cold Temperature IN of pass")
        self.Tc_out = Var(self.flowsheet().config.time,
                          self.PH,
                          initialize=390,
                          units=pyunits.K,
                          doc="Cold Temperature OUT of pass")

        # ======================================================================
        # PERFORMANCE EQUATIONS
        # mass flow rate in kg/s
        def rule_mh_in(blk, t):
            return blk.hot_side.properties_in[t].flow_mol *\
                blk.hot_side.properties_in[t].mw

        self.mh_in = Expression(self.flowsheet().config.time,
                                rule=rule_mh_in,
                                doc='Hotside mass flow rate [kg/s]')

        def rule_mc_in(blk, t):
            return blk.cold_side.properties_in[t].flow_mol *\
                blk.cold_side.properties_in[t].mw

        self.mc_in = Expression(self.flowsheet().config.time,
                                rule=rule_mc_in,
                                doc='Coldside mass flow rate [kg/s]')

        # ----------------------------------------------------------------------
        # port mass velocity[kg/m2.s]
        def rule_Gph(blk, t):
            return (4 * blk.mh_in[t] * 7) / (22 * blk.port_dia**2)

        self.Gph = Expression(self.flowsheet().config.time,
                              rule=rule_Gph,
                              doc='Hotside port mass velocity[kg/m2.s]')

        def rule_Gpc(blk, t):
            return (4 * blk.mc_in[t] * 7) / (22 * blk.port_dia**2)

        self.Gpc = Expression(self.flowsheet().config.time,
                              rule=rule_Gpc,
                              doc='Coldside port mass velocity[kg/m2.s]')

        # ----------------------------------------------------------------------
        # Reynold & Prandtl numbers
        def rule_Re_h(blk, t, p):
            return blk.mh_in[t] * blk.channel_dia /\
                (blk.Np[p] * blk.plate_width *
                 blk.plate_gap * blk.hot_side.properties_in[t].visc_d)

        self.Re_h = Expression(self.flowsheet().config.time,
                               self.PH,
                               rule=rule_Re_h,
                               doc='Hotside Reynolds number')

        def rule_Re_c(blk, t, p):
            return blk.mc_in[t] * blk.channel_dia /\
                (blk.Np[p] * blk.plate_width *
                 blk.plate_gap * blk.cold_side.properties_in[t].visc_d)

        self.Re_c = Expression(self.flowsheet().config.time,
                               self.PH,
                               rule=rule_Re_c,
                               doc='Coldside Reynolds number')

        def rule_Pr_h(blk, t):
            return blk.cp_hot[t] * blk.hot_side.properties_in[t].visc_d /\
                blk.hot_side.properties_in[t].thermal_cond

        self.Pr_h = Expression(self.flowsheet().config.time,
                               rule=rule_Pr_h,
                               doc='Hotside Prandtl number')

        def rule_Pr_c(blk, t):
            return blk.cp_cold[t] * blk.cold_side.properties_in[t].visc_d /\
                blk.cold_side.properties_in[t].thermal_cond

        self.Pr_c = Expression(self.flowsheet().config.time,
                               rule=rule_Pr_c,
                               doc='Coldside Prandtl number')

        # ----------------------------------------------------------------------
        # Film heat transfer coefficients

        def rule_hotside_transfer_coef(blk, t, p):
            return (blk.hot_side.properties_in[t].thermal_cond /
                    blk.channel_dia * blk.param_a *
                    blk.Re_h[t, p]**blk.param_b * blk.Pr_h[t]**blk.param_c)

        self.h_hot = Expression(self.flowsheet().config.time,
                                self.PH,
                                rule=rule_hotside_transfer_coef,
                                doc='Hotside heat transfer coefficient')

        def rule_coldside_transfer_coef(blk, t, p):
            return (blk.cold_side.properties_in[t].thermal_cond /
                    blk.channel_dia * blk.param_a *
                    blk.Re_c[t, p]**blk.param_b * blk.Pr_c[t]**blk.param_c)

        self.h_cold = Expression(self.flowsheet().config.time,
                                 self.PH,
                                 rule=rule_coldside_transfer_coef,
                                 doc='Coldside heat transfer coefficient')

        # ----------------------------------------------------------------------
        # Friction factor calculation
        def rule_fric_h(blk, t):
            return 18.29 * blk.Re_h[t, 1]**(-0.652)

        self.fric_h = Expression(self.flowsheet().config.time,
                                 rule=rule_fric_h,
                                 doc='Hotside friction factor')

        def rule_fric_c(blk, t):
            return 1.441 * self.Re_c[t, 1]**(-0.206)

        self.fric_c = Expression(self.flowsheet().config.time,
                                 rule=rule_fric_c,
                                 doc='Coldside friction factor')

        # ----------------------------------------------------------------------
        # pressure drop calculation
        def rule_hotside_dP(blk, t):
            return (2 * blk.fric_h[t] * (blk.plate_length + blk.port_dia) *
                    blk.P * blk.Gph[t]**2) /\
                (blk.hot_side.properties_in[t].dens_mass *
                 blk.channel_dia) + 1.4 * blk.P * blk.Gph[t]**2 * 0.5 /\
                blk.hot_side.properties_in[t].dens_mass + \
                blk.hot_side.properties_in[t].dens_mass * \
                9.81 * (blk.plate_length + blk.port_dia)

        self.dP_h = Expression(self.flowsheet().config.time,
                               rule=rule_hotside_dP,
                               doc='Hotside pressure drop  [Pa]')

        def rule_coldside_dP(blk, t):
            return (2 * blk.fric_c[t] * (blk.plate_length + blk.port_dia) *
                    blk.P * blk.Gpc[t]**2) /\
                (blk.cold_side.properties_in[t].dens_mass * blk.channel_dia) +\
                1.4 * (blk.P * blk.Gpc[t]**2 * 0.5 /
                       blk.cold_side.properties_in[t].dens_mass) + \
                blk.cold_side.properties_in[t].dens_mass * \
                9.81 * (blk.plate_length + blk.port_dia)

        self.dP_c = Expression(self.flowsheet().config.time,
                               rule=rule_coldside_dP,
                               doc='Coldside pressure drop  [Pa]')

        def rule_eq_deltaP_hot(blk, t):
            return blk.hot_side.deltaP[t] == -blk.dP_h[t]

        self.eq_deltaP_hot = Constraint(self.flowsheet().config.time,
                                        rule=rule_eq_deltaP_hot)

        def rule_eq_deltaP_cold(blk, t):
            return blk.cold_side.deltaP[t] == -blk.dP_c[t]

        self.eq_deltaP_cold = Constraint(self.flowsheet().config.time,
                                         rule=rule_eq_deltaP_cold)

        # ----------------------------------------------------------------------
        # Overall heat transfer coefficients
        def rule_U(blk, t, p):
            return 1.0 /\
                (1.0 / blk.h_hot[t, p] + blk.plate_gap / blk.plate_thermal_cond +
                 1.0 / blk.h_cold[t, p])

        self.U = Expression(self.flowsheet().config.time,
                            self.PH,
                            rule=rule_U,
                            doc='Overall heat transfer coefficient')

        # ----------------------------------------------------------------------
        # capacitance of hot and cold fluid

        def rule_Caph(blk, t, p):
            return blk.mh_in[t] * blk.cp_hot[t] / blk.Np[p]

        self.Caph = Expression(self.flowsheet().config.time,
                               self.PH,
                               rule=rule_Caph,
                               doc='Hotfluid capacitance rate')

        def rule_Capc(blk, t, p):
            return blk.mc_in[t] * blk.cp_cold[t] / blk.Np[p]

        self.Capc = Expression(self.flowsheet().config.time,
                               self.PH,
                               rule=rule_Capc,
                               doc='Coldfluid capacitance rate')

        # ----------------------------------------------------------------------
        # min n max capacitance and capacitance ratio
        def rule_Cmin(blk, t, p):
            return 0.5 * (blk.Caph[t, p] + blk.Capc[t, p] - (
                (blk.Caph[t, p] - blk.Capc[t, p])**2 + 0.00001)**0.5)

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

        def rule_Cmax(blk, t, p):
            return 0.5 * (blk.Caph[t, p] + blk.Capc[t, p] + (
                (blk.Caph[t, p] - blk.Capc[t, p])**2 + 0.00001)**0.5)

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

        def rule_CR(blk, t, p):
            return blk.Cmin[t, p] / blk.Cmax[t, p]

        self.CR = Expression(self.flowsheet().config.time,
                             self.PH,
                             rule=rule_CR,
                             doc='Capacitance ratio')

        # ----------------------------------------------------------------------
        # Number of Transfer units for sub heat exchanger
        def rule_NTU(blk, t, p):
            return blk.U[t, p] * blk.plate_area / blk.Cmin[t, p]

        self.NTU = Expression(self.flowsheet().config.time,
                              self.PH,
                              rule=rule_NTU,
                              doc='Number of Transfer Units')

        # ----------------------------------------------------------------------
        # effectiveness of sub-heat exchangers
        def rule_Ecf(blk, t, p):
            if blk.P.value % 2 == 0:
                return (1 - exp(-blk.NTU[t, p] * (1 - blk.CR[t, p]))) / \
                    (1 - blk.CR[t, p] *
                     exp(-blk.NTU[t, p] * (1 - blk.CR[t, p])))
            elif blk.P.value % 2 == 1:
                return (1 - exp(-blk.NTU[t, p] *
                                (1 + blk.CR[t, p]))) / (1 + blk.CR[t, p])

        self.Ecf = Expression(self.flowsheet().config.time,
                              self.PH,
                              rule=rule_Ecf,
                              doc='Effectiveness for sub-HX')

        # ----------------------------------------------------------------------
        # Energy balance equations for hot fluid in sub-heat exhanger
        def rule_Ebh_eq(blk, t, p):
            return blk.Th_out[t, p] == blk.Th_in[t, p] -\
                blk.Ecf[t, p] * blk.Cmin[t, p] / blk.Caph[t, p] * \
                (blk.Th_in[t, p] - blk.Tc_in[t, p])

        self.Ebh_eq = Constraint(
            self.flowsheet().config.time,
            self.PH,
            rule=rule_Ebh_eq,
            doc='Hot fluid sub-heat exchanger energy balance')

        # Hot fluid exit temperature
        def rule_Tout_hot(blk, t):
            return blk.Th_out[t, blk.P.value] ==\
                blk.hot_side.properties_out[t].temperature

        self.Tout_hot_eq = Constraint(self.flowsheet().config.time,
                                      rule=rule_Tout_hot,
                                      doc='Hot fluid exit temperature')

        # Energy balance equations for cold fluid in sub-heat exhanger
        def rule_Ebc_eq(blk, t, p):
            return blk.Tc_out[t, p] == blk.Tc_in[t, p] + \
                blk.Ecf[t, p] * blk.Cmin[t, p] / blk.Capc[t, p] * \
                (blk.Th_in[t, p] - blk.Tc_in[t, p])

        self.Ebc_eq = Constraint(
            self.flowsheet().config.time,
            self.PH,
            rule=rule_Ebc_eq,
            doc='Cold fluid sub-heat exchanger energy balance')

        # Cold fluid exit temperature
        def rule_Tout_cold(blk, t):
            return blk.Tc_out[t, 1] ==\
                blk.cold_side.properties_out[t].temperature

        self.Tout_cold_eq = Constraint(self.flowsheet().config.time,
                                       rule=rule_Tout_cold,
                                       doc='Cold fluid exit temperature')

        # ----------------------------------------------------------------------
        # Energy balance boundary conditions
        def rule_hot_BCIN(blk, t):
            return blk.Th_in[t, 1] == \
                blk.hot_side.properties_in[t].temperature

        self.hot_BCIN = Constraint(self.flowsheet().config.time,
                                   rule=rule_hot_BCIN,
                                   doc='Hot fluid inlet boundary conditions')

        def rule_cold_BCIN(blk, t):
            return blk.Tc_in[t, blk.P.value] ==\
                blk.cold_side.properties_in[t].temperature

        self.cold_BCIN = Constraint(self.flowsheet().config.time,
                                    rule=rule_cold_BCIN,
                                    doc='Cold fluid inlet boundary conditions')

        Pset = [i for i in range(1, self.P.value)]

        def rule_hot_BC(blk, t, p):
            return blk.Th_out[t, p] == blk.Th_in[t, p + 1]

        self.hot_BC = Constraint(
            self.flowsheet().config.time,
            Pset,
            rule=rule_hot_BC,
            doc='Hot fluid boundary conditions: change of pass')

        def rule_cold_BC(blk, t, p):
            return blk.Tc_out[t, p + 1] == blk.Tc_in[t, p]

        self.cold_BC = Constraint(
            self.flowsheet().config.time,
            Pset,
            rule=rule_cold_BC,
            doc='Cold fluid boundary conditions: change of pass')

        # ----------------------------------------------------------------------
        # Energy transferred
        def rule_QH(blk, t):
            return blk.mh_in[t] * blk.cp_hot[t] *\
                (blk.hot_side.properties_in[t].temperature -
                 blk.hot_side.properties_out[t].temperature)

        self.QH = Expression(self.flowsheet().config.time,
                             rule=rule_QH,
                             doc='Heat lost by hot fluid')

        def rule_QC(blk, t):
            return blk.mc_in[t] * blk.cp_cold[t] *\
                (blk.cold_side.properties_out[t].temperature -
                 blk.cold_side.properties_in[t].temperature)

        self.QC = Expression(self.flowsheet().config.time,
                             rule=rule_QH,
                             doc='Heat gain by cold fluid')

    def initialize(blk,
                   hotside_state_args=None,
                   coldside_state_args=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        '''
        Initialisation routine for PHE 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 initialization routine
            optarg : solver options dictionary object (default=None, use
                     default solver options)
            solver : str indicating which solver to use during
                     initialization (default = None)
        Returns:
            None
        '''
        # Set solver options
        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)

        hotside_state_args = {
            'flow_mol': value(blk.hot_inlet.flow_mol[0]),
            'temperature': value(blk.hot_inlet.temperature[0]),
            'pressure': value(blk.hot_inlet.pressure[0]),
            'mole_frac_comp': {
                'H2O': value(blk.hot_inlet.mole_frac_comp[0, 'H2O']),
                'CO2': value(blk.hot_inlet.mole_frac_comp[0, 'CO2']),
                'MEA': value(blk.hot_inlet.mole_frac_comp[0, 'MEA'])
            }
        }

        coldside_state_args = {
            'flow_mol': value(blk.cold_inlet.flow_mol[0]),
            'temperature': value(blk.cold_inlet.temperature[0]),
            'pressure': value(blk.cold_inlet.pressure[0]),
            'mole_frac_comp': {
                'H2O': value(blk.cold_inlet.mole_frac_comp[0, 'H2O']),
                'CO2': value(blk.cold_inlet.mole_frac_comp[0, 'CO2']),
                'MEA': value(blk.cold_inlet.mole_frac_comp[0, 'MEA'])
            }
        }

        # ---------------------------------------------------------------------
        # Initialize the INLET properties
        init_log.info('STEP 1: PROPERTY INITIALIZATION')
        init_log.info_high("INLET Properties initialization")
        blk.hot_side.properties_in.initialize(state_args=hotside_state_args,
                                              outlvl=outlvl,
                                              optarg=optarg,
                                              solver=solver,
                                              hold_state=True)
        blk.cold_side.properties_in.initialize(state_args=coldside_state_args,
                                               outlvl=outlvl,
                                               optarg=optarg,
                                               solver=solver,
                                               hold_state=True)

        # Initialize the OUTLET properties
        init_log.info_high("OUTLET Properties initialization")
        blk.hot_side.properties_out.initialize(state_args=hotside_state_args,
                                               outlvl=outlvl,
                                               optarg=optarg,
                                               solver=solver,
                                               hold_state=False)

        blk.cold_side.properties_out.initialize(state_args=coldside_state_args,
                                                outlvl=outlvl,
                                                optarg=optarg,
                                                solver=solver,
                                                hold_state=False)
        # ----------------------------------------------------------------------
        init_log.info('STEP 2: PHE INITIALIZATION')
        with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
            res = opt.solve(blk, tee=slc.tee)
        init_log.info_high("STEP 2 Complete: {}.".format(
            idaeslog.condition(res)))
        init_log.info('INITIALIZATION COMPLETED')
コード例 #8
0
class PFRData(UnitModelBlockData):
    """
    Standard Plug Flow Reactor Unit Model Class
    """
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare(
        "material_balance_type",
        ConfigValue(
            default=MaterialBalanceType.useDefault,
            domain=In(MaterialBalanceType),
            description="Material balance construction flag",
            doc="""Indicates what type of mass balance should be constructed,
**default** - MaterialBalanceType.useDefault.
**Valid values:** {
**MaterialBalanceType.useDefault - refer to property package for default
balance type
**MaterialBalanceType.none** - exclude material balances,
**MaterialBalanceType.componentPhase** - use phase component balances,
**MaterialBalanceType.componentTotal** - use total component balances,
**MaterialBalanceType.elementTotal** - use total element balances,
**MaterialBalanceType.total** - use total material balance.}"""))
    CONFIG.declare(
        "energy_balance_type",
        ConfigValue(
            default=EnergyBalanceType.useDefault,
            domain=In(EnergyBalanceType),
            description="Energy balance construction flag",
            doc="""Indicates what type of energy balance should be constructed,
**default** - EnergyBalanceType.useDefault.
**Valid values:** {
**EnergyBalanceType.useDefault - refer to property package for default
balance type
**EnergyBalanceType.none** - exclude energy balances,
**EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
**EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
**EnergyBalanceType.energyTotal** - single energy balance for material,
**EnergyBalanceType.energyPhase** - energy balances for each phase.}"""))
    CONFIG.declare(
        "momentum_balance_type",
        ConfigValue(
            default=MomentumBalanceType.pressureTotal,
            domain=In(MomentumBalanceType),
            description="Momentum balance construction flag",
            doc="""Indicates what type of momentum balance should be constructed,
**default** - MomentumBalanceType.pressureTotal.
**Valid values:** {
**MomentumBalanceType.none** - exclude momentum balances,
**MomentumBalanceType.pressureTotal** - single pressure balance for material,
**MomentumBalanceType.pressurePhase** - pressure balances for each phase,
**MomentumBalanceType.momentumTotal** - single momentum balance for material,
**MomentumBalanceType.momentumPhase** - momentum balances for each phase.}"""))
    CONFIG.declare(
        "has_equilibrium_reactions",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Equilibrium reaction construction flag",
            doc="""Indicates whether terms for equilibrium controlled reactions
should be constructed,
**default** - True.
**Valid values:** {
**True** - include equilibrium reaction terms,
**False** - exclude equilibrium reaction terms.}"""))
    CONFIG.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Phase equilibrium construction flag",
            doc="""Indicates whether terms for phase equilibrium should be
constructed,
**default** = False.
**Valid values:** {
**True** - include phase equilibrium terms
**False** - exclude phase equilibrium terms.}"""))
    CONFIG.declare(
        "has_heat_of_reaction",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Heat of reaction term construction flag",
            doc="""Indicates whether terms for heat of reaction terms should be
constructed,
**default** - False.
**Valid values:** {
**True** - include heat of reaction terms,
**False** - exclude heat of reaction terms.}"""))
    CONFIG.declare(
        "has_heat_transfer",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Heat transfer term construction flag",
            doc=
            """Indicates whether terms for heat transfer should be constructed,
**default** - False.
**Valid values:** {
**True** - include heat transfer terms,
**False** - exclude heat transfer terms.}"""))
    CONFIG.declare(
        "has_pressure_change",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
constructed,
**default** - False.
**Valid values:** {
**True** - include pressure change terms,
**False** - exclude pressure change terms.}"""))
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PropertyParameterObject** - a PropertyParameterBlock object.}"""))
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "reaction_package",
        ConfigValue(
            default=None,
            domain=is_reaction_parameter_block,
            description="Reaction package to use for control volume",
            doc=
            """Reaction parameter object used to define reaction calculations,
**default** - None.
**Valid values:** {
**None** - no reaction package,
**ReactionParameterBlock** - a ReactionParameterBlock object.}"""))
    CONFIG.declare(
        "reaction_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing reaction packages",
            doc=
            """A ConfigBlock with arguments to be passed to a reaction block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see reaction package for documentation.}"""))
    CONFIG.declare(
        "length_domain_set",
        ConfigValue(
            default=[0.0, 1.0],
            domain=ListOf(float),
            description="List of points to use to initialize length domain",
            doc=
            """A list of values to be used when constructing the length domain
of the reactor. Point must lie between 0.0 and 1.0,
**default** - [0.0, 1.0].
**Valid values:** {
a list of floats}"""))
    CONFIG.declare(
        "transformation_method",
        ConfigValue(
            default="dae.finite_difference",
            description="Method to use for DAE transformation",
            doc="""Method to use to transform domain. Must be a method recognised
by the Pyomo TransformationFactory,
**default** - "dae.finite_difference"."""))
    CONFIG.declare(
        "transformation_scheme",
        ConfigValue(default="BACKWARD",
                    description="Scheme to use for DAE transformation",
                    doc="""Scheme to use when transformating domain. See Pyomo
documentation for supported schemes,
**default** - "BACKWARD"."""))
    CONFIG.declare(
        "finite_elements",
        ConfigValue(
            default=20,
            description=
            "Number of finite elements to use for DAE transformation",
            doc="""Number of finite elements to use when transforming length
domain,
**default** - 20."""))
    CONFIG.declare(
        "collocation_points",
        ConfigValue(
            default=3,
            description="No. collocation points to use for DAE transformation",
            doc="""Number of collocation points to use when transforming length
domain,
**default** - 3."""))

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

        Args:
            None

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

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

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

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

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

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

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

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

        self.control_volume.apply_transformation()

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

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

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

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

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

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

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

        return {"vars": var_dict}

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

        self.costing = Block()
        units_meta = (
            self.config.property_package.get_metadata().get_derived_units)
        self.diameter = Var(initialize=1,
                            units=units_meta('length'),
                            doc='vessel diameter')
        self.diameter_eq = Constraint(
            expr=self.volume == (self.length * const.pi * self.diameter**2) /
            4)
        module.pfr_costing(self.costing, **kwargs)
コード例 #9
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)
コード例 #10
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")
コード例 #11
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.}""",
        ),
    )
    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)
コード例 #12
0
ファイル: reboiler.py プロジェクト: qzhe-mechanics/idaes-pse
class ReboilerData(UnitModelBlockData):
    """
    Reboiler unit for distillation model.
    Unit model to reboil the liquid from the bottom tray of
    the distillation column.
    """
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare(
        "has_boilup_ratio",
        ConfigValue(default=False,
                    domain=In([True, False]),
                    description="Boilup ratio term construction flag",
                    doc="""Indicates whether terms for boilup ratio should be
constructed,
**default** - False.
**Valid values:** {
**True** - include construction of boilup ratio constraint,
**False** - exclude construction of boilup ratio constraint}"""))
    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(ReboilerData, self).build()

        # Add Control Volume for the Reboiler
        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)

        if self.config.has_boilup_ratio is True:

            self.boilup_ratio = Var(initialize=0.5,
                                    doc="Boilup ratio for reboiler")

            def rule_boilup_ratio(self, t):
                if hasattr(self.control_volume.properties_out[t],
                           "flow_mol_phase"):
                    return self.boilup_ratio * \
                        sum(self.control_volume.properties_out[t].
                            flow_mol_phase[p] for p in self._liquid_set) == \
                        sum(self.control_volume.
                            properties_out[t].flow_mol_phase["Vap"]
                            for p in self._vapor_set)
                elif hasattr(self.control_volume.properties_out[t],
                             "flow_mol_phase_comp"):
                    return self.boilup_ratio * \
                        sum(self.control_volume.properties_out[t].
                            flow_mol_phase_comp[p, i]
                            for p in self._liquid_set
                            for i in self.control_volume.properties_out[t].
                            params.component_list) == \
                        sum(self.control_volume.properties_out[t].
                            flow_mol_phase_comp[p, i]
                            for p in self._vapor_set
                            for i in self.control_volume.properties_out[t].
                            params.component_list)
                else:
                    raise PropertyNotSupportedError(
                        "Unrecognized names for flow variables encountered "
                        "while building the constraint for reboiler.")

            self.eq_boilup_ratio = Constraint(self.flowsheet().time,
                                              rule=rule_boilup_ratio)

        self._make_ports()

        self._make_splits_reboiler()

        # 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 reboiler
        # Inlet port (the vapor from the top tray)
        self.add_inlet_port()

        # Outlet ports that always exist irrespective of reboiler type
        self.bottoms = Port(noruleinit=True, doc="Bottoms stream.")

        self.vapor_reboil = Port(noruleinit=True,
                                 doc="Vapor outlet stream that is returned to "
                                 "to the bottom tray.")

    def _make_splits_reboiler(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:

            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.control_volume.properties_out[:].\
                        component(local_name)
                else:
                    var = self.control_volume.properties_out[:].\
                        component(local_name)[...]

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

                # add the reference and variable name to the
                # vapor outlet port
                self.vapor_reboil.add(Reference(var), 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.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_volum.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 reboiler. ")
                    elif "mass" in local_name:
                        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_volum.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 reboiler.")
                    else:
                        raise PropertyNotSupportedError(
                            "No mass frac or mole frac variables encountered "
                            " while building ports for the reboiler. "
                            "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
                    # distillate port
                    self.bottoms.add(self.e_liq_frac, k)

                    # add the reference and variable name to the
                    # vapor port
                    self.vapor_reboil.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(local_name)[...]

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

                    # add the reference and variable name to the boil up port
                    self.vapor_reboil.add(Reference(var), 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 for vap flow
                        def rule_vap_flow(self, t):
                            return sum(self.control_volume.properties_out[t].
                                       component(local_name_flow)[p]
                                       for p in self._vapor_set)

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

                        # Rule to link the liq flow to the distillate
                        def rule_bottoms_flow(self, t):
                            return sum(self.control_volume.properties_out[t].
                                       component(local_name_flow)[p]
                                       for p in self._liquid_set)

                        self.e_bottoms_flow = Expression(
                            self.flowsheet().time, rule=rule_bottoms_flow)

                    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 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_flow)[p, i]
                                       for p in self._vapor_set)

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

                        # Rule for liq phase flow to the liquid outlet
                        def rule_bottoms_flow(self, t, i):
                            return sum(self.control_volume.properties_out[t].
                                       component(local_name_flow)[p, i]
                                       for p in self._liquid_set)

                        self.e_bottoms_flow = Expression(
                            self.flowsheet().time,
                            index_set,
                            rule=rule_bottoms_flow)

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

                    # add the reference and variable name to the
                    # distillate port
                    self.vapor_reboil.add(self.e_vap_flow, k)
                else:
                    # when it is flow indexed by phase or indexed by
                    # both phase and component.
                    var = self.control_volume.properties_out[:].\
                        component(local_name)[...]

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

                    # add the reference and variable name to the
                    # vapor outlet port
                    self.vapor_reboil.add(Reference(var), k)
            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_enth = 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 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_enth)[p] for p in self._vapor_set)

                    self.e_vap_enth = Expression(self.flowsheet().time,
                                                 rule=rule_vap_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_bottoms_enth(self, t):
                        return sum(
                            self.control_volume.properties_out[t].component(
                                local_name_enth)[p] for p in self._liquid_set)

                    self.e_bottoms_enth = Expression(self.flowsheet().time,
                                                     rule=rule_bottoms_enth)

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

                    # add the reference and variable name to the
                    # distillate port
                    self.vapor_reboil.add(self.e_vap_enth, 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

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

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

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

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

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

        if solver is None:
            init_log.warning("Solver not provided. Default solver(ipopt) "
                             " being used for initialization.")
            solver = get_default_solver()

        # Initialize the inlet and outlet state blocks. Calling the state
        # blocks initialize methods directly so that custom set of state args
        # can be passed to the inlet and outlet state blocks as control_volume
        # initialize method initializes the state blocks with the same
        # state conditions.
        flags = self.control_volume.properties_in. \
            initialize(state_args=state_args,
                       solver=solver,
                       optarg=optarg,
                       outlvl=outlvl,
                       hold_state=True)

        # Initialize outlet state block at same conditions of inlet except
        # the temperature. Set the temperature to a temperature guess based
        # on the desired boilup_ratio.

        # Get index for bubble point temperature and and assume it
        # will have only a single phase equilibrium pair. This is to
        # support the generic property framework where the T_bubble
        # is indexed by the phases_in_equilibrium. In distillation,
        # the assumption is that there will only be a single pair
        # i.e. vap-liq.
        idx = next(
            iter(self.control_volume.properties_in[0].temperature_bubble))
        temp_guess = 0.5 * (
            value(self.control_volume.properties_in[0].temperature_dew[idx]) -
            value(self.control_volume.properties_in[0].
                  temperature_bubble[idx])) + \
            value(self.control_volume.properties_in[0].temperature_bubble[idx])

        state_args_outlet = {}
        state_dict_outlet = (self.control_volume.properties_in[
            self.flowsheet().config.time.first()].define_port_members())

        for k in state_dict_outlet.keys():
            if state_dict_outlet[k].is_indexed():
                state_args_outlet[k] = {}
                for m in state_dict_outlet[k].keys():
                    state_args_outlet[k][m] = value(state_dict_outlet[k][m])
            else:
                if k != "temperature":
                    state_args_outlet[k] = value(state_dict_outlet[k])
                else:
                    state_args_outlet[k] = temp_guess

        self.control_volume.properties_out.initialize(
            state_args=state_args_outlet,
            solver=solver,
            optarg=optarg,
            outlvl=outlvl,
            hold_state=False)

        if degrees_of_freedom(self) == 0:
            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:
            raise ConfigurationError(
                "State vars fixed but degrees of freedom "
                "for reboiler is not zero during "
                "initialization. Please ensure that the boilup_ratio "
                "or the outlet temperature is fixed.")

        self.control_volume.properties_in.\
            release_state(flags=flags, outlvl=outlvl)

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

        return {"vars": var_dict}

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

        stream_dict = {
            "Inlet": "inlet",
            "Vapor Reboil": "vapor_reboil",
            "Bottoms": "bottoms"
        }

        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")
コード例 #13
0
class PIDControllerData(UnitModelBlockData):
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare(
        "pv",
        ConfigValue(
            default=None,
            description="Process variable to be controlled",
            doc="A Pyomo Var, Expression, or Reference for the measured"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "mv",
        ConfigValue(
            default=None,
            description="Manipulated process variable",
            doc="A Pyomo Var, Expression, or Reference for the controlled"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "bounded_output",
        ConfigValue(
            default=False,
            description="Flag to bound manipulated variable",
            doc=
            """Indicating if the output for the manipulated variable is bounded.
Default: False.  If True, user need to set the lower and upper bound parameters"""
        ))
    CONFIG.declare(
        "type",
        ConfigValue(default="PI",
                    domain=In(['P', 'PI', 'PD', 'PID']),
                    description="Control type",
                    doc="""Controller type options including
- P: Proportional only
- PI: Proportional and integral only
- PD: Proportional and derivative only
- PID: Proportional, integral and derivative
Default is PI"""))

    def build(self):
        """
        Build the PID block
        """
        super().build()  # do the ProcessBlockData voodoo for config

        # Do nothing if steady-state
        if self.config.dynamic == True:
            # Check for required config
            if self.config.pv is None:
                raise ConfigurationError(
                    "Controller configuration requires 'pv'")
            if self.config.mv is None:
                raise ConfigurationError(
                    "Controller configuration requires 'mv'")
            # Shorter pointers to time set information
            time_set = self.flowsheet().config.time
            self.pv = Reference(self.config.pv)  # No duplicate
            self.mv = Reference(self.config.mv)  # No duplicate

            # Parameters
            self.smooth_eps = Param(
                mutable=True,
                initialize=1e-4,
                doc="Smoothing parameter for controller output limits")
            self.mv_lb = Param(
                mutable=True,
                initialize=0.05,
                doc="controller output lower bound"
            )  #not use 0 for valve since it could cause negative flow
            self.mv_ub = Param(mutable=True,
                               initialize=1,
                               doc="controller output upper bound")

            # Variable for basic controller settings may change with time.
            self.setpoint = Var(time_set, initialize=0.5, doc="Setpoint")
            self.gain_p = Var(time_set,
                              initialize=0.1,
                              doc="Gain for proportional part")
            if self.config.type == 'PI' or self.config.type == 'PID':
                self.gain_i = Var(time_set,
                                  initialize=0.1,
                                  doc="Gain for integral part")
            if self.config.type == 'PD' or self.config.type == 'PID':
                self.gain_d = Var(time_set,
                                  initialize=0.01,
                                  doc="Gain for derivative part")
            self.mv_ref = Var(initialize=0.5,
                              doc="bias value of manipulated variable")

            if self.config.type == 'P' or self.config.type == 'PI':

                @self.Expression(time_set, doc="Error expression")
                def error(b, t):
                    return b.setpoint[t] - b.pv[t]
            else:
                self.error = Var(time_set, initialize=0, doc="Error variable")

                @self.Constraint(time_set, doc="Error variable")
                def error_eqn(b, t):
                    return b.error[t] == b.setpoint[t] - b.pv[t]

            if self.config.type == 'PI' or self.config.type == 'PID':
                self.integral_of_error = Var(time_set,
                                             initialize=0,
                                             doc="Integral term")
                self.error_from_integral = DerivativeVar(
                    self.integral_of_error,
                    wrt=self.flowsheet().config.time,
                    initialize=0)

                @self.Constraint(
                    time_set, doc="Error calculated by derivative of integral")
                def error_from_integral_eqn(b, t):
                    return b.error[t] == b.error_from_integral[t]

            if self.config.type == 'PID' or self.config.type == 'PD':
                self.derivative_of_error = DerivativeVar(
                    self.error, wrt=self.flowsheet().config.time, initialize=0)

            @self.Expression(time_set, doc="Proportional output")
            def mv_p_only(b, t):
                return b.gain_p[t] * b.error[t]

            @self.Expression(time_set, doc="Proportional output and reference")
            def mv_p_only_with_ref(b, t):
                return b.gain_p[t] * b.error[t] + b.mv_ref

            if self.config.type == 'PI' or self.config.type == 'PID':

                @self.Expression(time_set, doc="Integral output")
                def mv_i_only(b, t):
                    return b.gain_i[t] * b.integral_of_error[t]

            if self.config.type == 'PD' or self.config.type == 'PID':

                @self.Expression(time_set, doc="Derivative output")
                def mv_d_only(b, t):
                    return b.gain_d[t] * b.derivative_of_error[t]

            @self.Expression(time_set,
                             doc="Unbounded output for manimulated variable")
            def mv_unbounded(b, t):
                if self.config.type == 'PID':
                    return b.mv_ref + b.gain_p[t] * b.error[t] + b.gain_i[
                        t] * b.integral_of_error[t] + b.gain_d[
                            t] * b.derivative_of_error[t]
                elif self.config.type == 'PI':
                    return b.mv_ref + b.gain_p[t] * b.error[t] + b.gain_i[
                        t] * b.integral_of_error[t]
                elif self.config.type == 'PD':
                    return b.mv_ref + b.gain_p[t] * b.error[t] + b.gain_d[
                        t] * b.derivative_of_error[t]
                else:
                    return b.mv_ref + b.gain_p[t] * b.error[t]

            @self.Constraint(time_set,
                             doc="Bounded output of manipulated variable")
            def mv_eqn(b, t):
                if t == b.flowsheet().config.time.first():
                    return Constraint.Skip
                else:
                    if self.config.bounded_output == True:
                        #return b.mv[t] == smooth_min(smooth_max(b.mv_unbounded[t], b.mv_lb, b.smooth_eps), b.mv_ub, b.smooth_eps)
                        return (b.mv[t] - b.mv_lb) * (1 + exp(
                            -4 / (b.mv_ub - b.mv_lb) *
                            (b.mv_unbounded[t] -
                             (b.mv_lb + b.mv_ub) / 2))) == b.mv_ub - b.mv_lb
                    else:
                        return b.mv[t] == b.mv_unbounded[t]

            @self.Expression(time_set,
                             doc="integral error at error 0 and mv_ref")
            def integral_of_error_ref(b, t):
                return ((b.mv_lb + b.mv_ub) / 2 - b.mv_ref -
                        log((b.mv_ub - b.mv_lb) /
                            (b.mv_ref - b.mv_lb) - 1) / 4 *
                        (b.mv_ub - b.mv_lb)) / b.gain_i[t]
コード例 #14
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)

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

        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.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
        """
        # 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["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.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.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]
                )

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

        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
            )
        else:
            # Call the general initialization routine in UnitModelBlockData
            super(PressureChangerData, blk).initialize(
                state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg
            )
        # if costing block exists, activate
        try:
            blk.costing.activate()
        except AttributeError:
            pass

    def init_isentropic(blk, state_args, outlvl, solver, optarg):
        """
        Initialization 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 initialization routine
            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

        Returns:
            None
        """
        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 holdup block
        flags = blk.control_volume.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )
        init_log.info_high("Initialization Step 1 Complete.")
        # ---------------------------------------------------------------------
        # Initialize Isentropic block

        # Set state_args from inlet state
        if state_args is None:
            state_args = {}
            state_dict = blk.control_volume.properties_in[
                blk.flowsheet().config.time.first()
            ].define_port_members()

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

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

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

        # ---------------------------------------------------------------------
        # Release Inlet state
        blk.control_volume.release_state(flags, outlvl + 1)
        init_log.info(
            "Initialization Complete: {}".format(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, Mat_factor="stain_steel",
                    mover_type="compressor",
                    compressor_type="centrifugal",
                    driver_mover_type="electrical_motor",
                    pump_type="centrifugal",
                    pump_type_factor='1.4',
                    pump_motor_type_factor='open',
                    year=None):
        if not hasattr(self.flowsheet(), "costing"):
            self.flowsheet().get_costing(year=year)

        self.costing = Block()
        module.pressure_changer_costing(self.costing, Mat_factor=Mat_factor,
                                        mover_type=mover_type,
                                        compressor_type=compressor_type,
                                        driver_mover_type=driver_mover_type,
                                        pump_type=pump_type,
                                        pump_type_factor=pump_type_factor,
                                        pump_motor_type_factor=pump_motor_type_factor)

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

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

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

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

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

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

        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))
コード例 #15
0
class SteamHeaterData(UnitModelBlockData):
    """
    WaterwallSection Unit Class
    """
    CONFIG = UnitModelBlockData.CONFIG()

    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=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,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "single_side_only",
        ConfigValue(
            default=True,
            domain=In([True, False]),
            description=
            "Flag indicating the heat is from one side of tubes only",
            doc="""Indicates whether tubes are heated from one side only,
**default** - True.
**Valid values:** {
**True** - single side is heated such as roof,
**False** - both sides are heated such as platen superheater.}"""))

    def build(self):
        """
        Build control volume and ports
        """
        # Call UnitModel.build to setup dynamics
        super(SteamHeaterData, self).build()

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

        self.control_volume.add_geometry()

        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)
        if (self.config.has_pressure_change is True
                and self.config.momentum_balance_type != 'none'):
            self.deltaP = Reference(self.control_volume.deltaP)

        # Set Unit Geometry and Holdup Volume
        self._set_geometry()

        # Construct performance equations
        self._make_performance()

    def _set_geometry(self):
        """
        Define the geometry of the unit as necessary, and link to holdup volume
        """
        # Number of tubes that steam flows through
        self.number_tubes = Var(initialize=4, doc="Number of tubes")

        # Average length of tubes that steam flows through from inlet to outlet
        self.tube_length = Var(initialize=5.0,
                               doc="length tube from inlet to outlet")

        # Inside diameter of tubes
        self.diameter_in = Var(initialize=0.05, doc="Inside diameter of tubes")

        # Inside radius of tube
        @self.Expression(doc="Inside radius of tube")
        def radius_in(b):
            return 0.5 * b.diameter_in

        # Total cross section area of fluid flow
        @self.Expression(doc="Cross section area of fluid")
        def area_cross_fluid_total(b):
            return 0.25 * const.pi * b.diameter_in**2 * b.number_tubes

        # Tube thickness
        self.tube_thickness = Var(initialize=0.005, doc="Thickness of tube")

        # Outside radius of tube
        @self.Expression(doc="Outside radius of tube")
        def radius_out(b):
            return b.radius_in + b.tube_thickness

        # Thickness of fin
        self.fin_thickness = Var(initialize=0.004, doc="Thickness of fin")

        # Length of fin
        self.fin_length = Var(initialize=0.005, doc="Length of fin")
        # Thickness of slag layer
        self.slag_thickness = Var(self.flowsheet().config.time,
                                  initialize=0.001,
                                  doc="thickness of slag layer")

        @self.Expression(doc="Pitch of two neighboring tubes")
        def pitch(b):
            return b.fin_length + b.radius_out * 2.0

        # total projected area
        @self.Expression(doc="total projected area for heat transfer")
        def area_proj_total(b):
            if self.config.single_side_only:
                return b.tube_length * b.pitch * b.number_tubes
            else:
                return 2 * b.tube_length * b.pitch * b.number_tubes

        @self.Expression(doc="Angle at joint of tube and fin")
        def alpha_tube(b):
            return asin(0.5 * b.fin_thickness / b.radius_out)

        @self.Expression(self.flowsheet().config.time,
                         doc="Angle at joint of tube "
                         "and fin at outside slag layer")
        def alpha_slag(b, t):
            return asin((0.5 * b.fin_thickness + b.slag_thickness[t]) /
                        (b.radius_out + b.slag_thickness[t]))

        @self.Expression(doc="Perimeter of interface between slag and tube")
        def perimeter_if(b):
            if self.config.single_side_only:
                return (const.pi - 2 * b.alpha_tube) * b.radius_out + b.pitch \
                    - 2 * b.radius_out * cos(b.alpha_tube)
            else:
                return 2 * ((const.pi - 2 * b.alpha_tube) * b.radius_out +
                            b.pitch - 2 * b.radius_out * cos(b.alpha_tube))

        @self.Expression(doc="Perimeter on the inner tube side")
        def perimeter_ts(b):
            return const.pi * b.diameter_in

        @self.Expression(self.flowsheet().config.time,
                         doc="Perimeter on the outer slag side")
        def perimeter_ss(b, t):
            if self.config.single_side_only:
                return (const.pi - 2 * b.alpha_slag[t]) \
                    * (b.radius_out + b.slag_thickness[t]) + \
                    b.pitch - 2 * (b.radius_out + b.slag_thickness[t]) \
                    * cos(b.alpha_slag[t])
            else:
                return 2 * (
                    (const.pi - 2 * b.alpha_slag[t]) *
                    (b.radius_out + b.slag_thickness[t]) + b.pitch - 2 *
                    (b.radius_out + b.slag_thickness[t]) *
                    cos(b.alpha_slag[t]))

        # Cross section area of tube and fin metal
        @self.Expression(doc="Cross section area of tube and fin metal")
        def area_cross_metal(b):
            return const.pi * (b.radius_out**2 - b.radius_in**2) \
                + b.fin_thickness * b.fin_length

        # Cross section area of slag layer
        @self.Expression(self.flowsheet().config.time,
                         doc="Cross section area of slag layer per tube")
        def area_cross_slag(b, t):
            return b.perimeter_if * b.slag_thickness[t]

        # Volume constraint
        @self.Constraint(self.flowsheet().config.time,
                         doc="waterwall fluid volume of all tubes")
        def volume_eqn(b, t):
            return b.volume[t] == 0.25 * const.pi * b.diameter_in**2 \
                * b.tube_length * b.number_tubes

    def _make_performance(self):
        """
        Define constraints which describe the behaviour of the unit model.
        """
        # Thermal conductivity of metal
        self.therm_cond_metal = Param(initialize=43.0,
                                      mutable=True,
                                      doc='Thermal conductivity of tube metal')
        # Thermal conductivity of slag
        self.therm_cond_slag = Var(initialize=1.3,
                                   doc='Thermal conductivity of slag')
        # Heat capacity of metal
        self.cp_metal = Param(initialize=500.0,
                              mutable=True,
                              doc='Heat capacity of tube metal')
        # Heat Capacity of slag
        self.cp_slag = Param(initialize=250,
                             mutable=True,
                             doc='Heat capacity of slag')
        # Density of metal
        self.dens_metal = Param(initialize=7800.0,
                                mutable=True,
                                doc='Density of tube metal')
        # Density of slag
        self.dens_slag = Param(initialize=2550,
                               mutable=True,
                               doc='Density of slag')
        # Shape factor of tube metal conduction
        self.fshape_metal = Param(initialize=1.0,
                                  mutable=True,
                                  doc='Shape factor of tube metal conduction')
        # Shape factor of slag conduction
        self.fshape_slag = Param(initialize=1.0,
                                 mutable=True,
                                 doc='Shape factor of slag conduction')

        # Add performance variables
        # Heat from fire side boiler model
        self.heat_fireside = Var(
            self.flowsheet().config.time,
            initialize=1e7,
            doc='total heat from fire side model for the section')
        # Tube boundary wall temperature
        self.temp_tube_boundary = Var(self.flowsheet().config.time,
                                      initialize=400.0,
                                      doc='Temperature of tube boundary wall')
        # Tube center point wall temperature
        self.temp_tube_center = Var(self.flowsheet().config.time,
                                    initialize=450.0,
                                    doc='Temperature of tube center wall')
        # Slag boundary wall temperature
        self.temp_slag_boundary = Var(self.flowsheet().config.time,
                                      initialize=600.0,
                                      doc='Temperature of slag boundary wall')
        # Slag center point slag wall temperature
        self.temp_slag_center = Var(
            self.flowsheet().config.time,
            initialize=500.0,
            doc='Temperature of slag layer center point')

        # Energy holdup for slag layer
        self.energy_holdup_slag = Var(self.flowsheet().config.time,
                                      initialize=1.0,
                                      doc='Energy holdup of slag layer')

        # Energy holdup for metal (tube + fin)
        self.energy_holdup_metal = Var(self.flowsheet().config.time,
                                       initialize=1.0,
                                       doc='Energy holdup of metal')

        # Energy accumulation for slag and metal
        if self.config.dynamic is True:
            self.energy_accumulation_slag = DerivativeVar(
                self.energy_holdup_slag,
                wrt=self.flowsheet().config.time,
                doc='Energy accumulation of slag layer')
            self.energy_accumulation_metal = DerivativeVar(
                self.energy_holdup_metal,
                wrt=self.flowsheet().config.time,
                doc='Energy accumulation of tube and fin metal')

        def energy_accumulation_term_slag(b, t):
            return b.energy_accumulation_slag[t] if b.config.dynamic else 0

        def energy_accumulation_term_metal(b, t):
            return b.energy_accumulation_metal[t] if b.config.dynamic else 0

        # Velocity of steam
        self.velocity = Var(self.flowsheet().config.time,
                            initialize=3.0,
                            doc='Velocity of steam')

        # Reynolds number based on liquid only flow
        self.N_Re = Var(self.flowsheet().config.time,
                        initialize=1.0e6,
                        doc='Reynolds number')

        # Prandtl number of liquid phase
        self.N_Pr = Var(self.flowsheet().config.time,
                        initialize=2.0,
                        doc='Reynolds number')

        # Darcy friction factor
        self.friction_factor_darcy = Var(self.flowsheet().config.time,
                                         initialize=0.01,
                                         doc='Darcy friction factor')

        # Convective heat transfer coefficient on tube side,
        # typically in range (1000, 5e5)
        self.hconv = Var(self.flowsheet().config.time,
                         initialize=30000.0,
                         doc='Convective heat transfer coefficient')

        # Convective heat flux to fluid
        self.heat_flux_conv = Var(self.flowsheet().config.time,
                                  initialize=7e4,
                                  doc='Convective heat flux to fluid')

        # Fire-side heat flux
        self.heat_flux_fireside = Var(
            self.flowsheet().config.time,
            initialize=100000.0,
            doc='Fireside heat flux to slag boundary')

        # Slag-tube interface heat flux
        self.heat_flux_interface = Var(self.flowsheet().config.time,
                                       initialize=100000.0,
                                       doc='Slag-tube interface heat flux')

        # Equation to calculate heat flux to slag boundary
        @self.Constraint(self.flowsheet().config.time,
                         doc="heat flux at slag outer layer")
        def heat_flux_fireside_from_boiler_eqn(b, t):
            if self.config.single_side_only:
                return b.heat_flux_fireside[t] * b.area_proj_total \
                    * b.perimeter_ss[t] == b.heat_fireside[t] * b.pitch
            else:
                return b.heat_flux_fireside[t] * b.area_proj_total \
                    * b.perimeter_ss[t] == b.heat_fireside[t] * b.pitch * 2.0

        # Equation to calculate slag layer boundary temperature
        @self.Constraint(self.flowsheet().config.time,
                         doc="slag layer boundary temperature")
        def slag_layer_boundary_temperature_eqn(b, t):
            return b.heat_flux_fireside[t] * 0.5 * b.slag_thickness[t] == \
                b.fshape_slag * b.therm_cond_slag * (b.temp_slag_boundary[t]
                                                     - b.temp_slag_center[t])

        # Equation to calculate heat flux at the slag-metal interface
        @self.Constraint(self.flowsheet().config.time,
                         doc="heat flux at slag-tube interface")
        def heat_flux_interface_eqn(b, t):
            return b.heat_flux_interface[t] * 0.5 * \
                   (b.slag_thickness[t]/b.therm_cond_slag/b.fshape_slag
                    + b.tube_thickness/b.therm_cond_metal/b.fshape_metal) == \
                   b.temp_slag_center[t] - b.temp_tube_center[t]

        # Equation to calculate heat flux at tube boundary
        @self.Constraint(self.flowsheet().config.time,
                         doc="convective heat flux at tube boundary")
        def heat_flux_conv_eqn(b, t):
            return b.heat_flux_conv[t] == \
                   b.hconv[t] * (b.temp_tube_boundary[t]
                                 - (b.control_volume.properties_in[t].
                                    temperature
                                    + b.control_volume.properties_out[t].
                                    temperature)/2.0)

        # Equation to calculate tube boundary wall temperature
        @self.Constraint(self.flowsheet().config.time,
                         doc="tube bounary wall temperature")
        def temperature_tube_boundary_eqn(b, t):
            return b.heat_flux_conv[t] * 0.5 * b.tube_thickness == \
                b.fshape_metal * b.therm_cond_metal \
                * (b.temp_tube_center[t] - b.temp_tube_boundary[t])

        # Equation to calculate energy holdup for slag layer per tube length
        @self.Constraint(self.flowsheet().config.time,
                         doc="energy holdup for slag layer")
        def energy_holdup_slag_eqn(b, t):
            return b.energy_holdup_slag[t] == \
                b.temp_slag_center[t] * b.cp_slag * b.dens_slag \
                * b.area_cross_slag[t]

        # Equation to calculate energy holdup for metal
        # (tube + fin) per tube length
        @self.Constraint(self.flowsheet().config.time,
                         doc="energy holdup for metal")
        def energy_holdup_metal_eqn(b, t):
            return b.energy_holdup_metal[t] == b.temp_tube_center[t] \
                * b.cp_metal * b.dens_metal * b.area_cross_metal

        # Energy balance for slag layer
        @self.Constraint(self.flowsheet().config.time,
                         doc="energy balance for slag layer")
        def energy_balance_slag_eqn(b, t):
            return energy_accumulation_term_slag(b, t) == \
                (b.heat_flux_fireside[t] * b.perimeter_ss[t]
                 - b.heat_flux_interface[t] * b.perimeter_if)

        # Energy balance for metal
        @self.Constraint(self.flowsheet().config.time,
                         doc="energy balance for metal")
        def energy_balance_metal_eqn(b, t):
            return energy_accumulation_term_metal(
                b, t) == (b.heat_flux_interface[t] * b.perimeter_if -
                          b.heat_flux_conv[t] * b.perimeter_ts)

        # Expression to calculate slag/tube metal interface wall temperature
        @self.Expression(self.flowsheet().config.time,
                         doc="Slag tube interface wall temperature")
        def temp_interface(b, t):
            return b.temp_tube_center[t] + b.heat_flux_interface[t] * 0.5 \
                * b.tube_thickness/b.therm_cond_metal/b.fshape_metal

        # Equations for calculate pressure drop
        # and convective heat transfer coefficient
        # Equation for calculating velocity
        @self.Constraint(self.flowsheet().config.time, doc="Vecolity of fluid")
        def velocity_eqn(b, t):
            return 1e-3*b.velocity[t]*b.area_cross_fluid_total * \
                   b.control_volume.properties_in[t].dens_mol_phase["Vap"] \
                   == 1e-3*b.control_volume.properties_in[t].flow_mol

        # Equation for calculating Reynolds number if liquid only
        @self.Constraint(self.flowsheet().config.time, doc="Reynolds number")
        def Reynolds_number_eqn(b, t):
            return b.N_Re[t] * \
                   b.control_volume.properties_in[t].visc_d_phase["Vap"] == \
                   b.diameter_in * b.velocity[t] * \
                   b.control_volume.properties_in[t].dens_mass

        # Friction factor depending on laminar or turbulent flow
        @self.Constraint(self.flowsheet().config.time,
                         doc="Darcy friction factor")
        def friction_factor_darcy_eqn(b, t):
            return Expr_if(
                b.N_Re[t] < 1187.384,
                b.friction_factor_darcy[t] * b.N_Re[t] / 64.0,
                b.friction_factor_darcy[t] * b.N_Re[t]**0.25 / 0.3164) == 1.0

        # Pressure change equation due to friction,
        # -1/2*density*velocity^2*fD/diameter*length
        @self.Constraint(self.flowsheet().config.time,
                         doc="pressure change due to friction")
        def pressure_change_eqn(b, t):
            return b.deltaP[t] * b.diameter_in == \
                -0.5 * b.control_volume.properties_in[t].dens_mass * \
                b.velocity[t]**2 * b.friction_factor_darcy[t] \
                * b.tube_length

        # Total heat added to control_volume
        @self.Constraint(self.flowsheet().config.time,
                         doc="total heat added to fluid control_volume")
        def heat_eqn(b, t):
            return b.heat_duty[t] == b.number_tubes * b.heat_flux_conv[t] \
                * b.tube_length * b.perimeter_ts

        # Prandtl number of steam
        @self.Constraint(self.flowsheet().config.time, doc="Prandtl number")
        def N_Pr_eqn(b, t):
            return b.N_Pr[t] \
                * b.control_volume.properties_in[t].therm_cond_phase["Vap"] \
                * b.control_volume.properties_in[t].mw == \
                b.control_volume.properties_in[t].cp_mol_phase["Vap"] * \
                b.control_volume.properties_in[t].visc_d_phase["Vap"]

        # Forced convection heat transfer coefficient for liquid only
        @self.Constraint(self.flowsheet().config.time,
                         doc="forced convection heat transfer"
                         " coefficient for liquid only")
        def hconv_eqn(b, t):
            return b.hconv[t] * b.diameter_in == 0.023 * b.N_Re[t]**0.8 \
                * b.N_Pr[t]**0.4 * \
                b.control_volume.properties_in[t].therm_cond_phase["Vap"]

    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.energy_accumulation_slag[:].value = 0
            self.energy_accumulation_metal[:].value = 0
            self.control_volume.material_accumulation[0, :, :].fix(0)
            self.control_volume.energy_accumulation[0, :].fix(0)
            self.energy_accumulation_slag[0].fix(0)
            self.energy_accumulation_metal[0].fix(0)

    def initialize(blk,
                   state_args=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        '''
        Waterwall section 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

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

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

        # Fix outlet enthalpy and pressure
        for t in blk.flowsheet().config.time:
            blk.control_volume.properties_out[t].enth_mol.fix(
                value(blk.control_volume.properties_in[t].enth_mol) +
                value(blk.heat_fireside[t]) /
                value(blk.control_volume.properties_in[t].flow_mol))
            blk.control_volume.properties_out[t].pressure.fix(
                value(blk.control_volume.properties_in[t].pressure) - 1.0)

        blk.heat_eqn.deactivate()
        blk.pressure_change_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().config.time:
            blk.control_volume.properties_out[t].enth_mol.unfix()
            blk.control_volume.properties_out[t].pressure.unfix()
        blk.heat_eqn.activate()
        blk.pressure_change_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=outlvl)
        init_log.info("Initialization Complete.")

    def calculate_scaling_factors(self):
        super().calculate_scaling_factors()
        for v in self.N_Re.values():
            if iscale.get_scaling_factor(v, warning=True) is None:
                iscale.set_scaling_factor(v, 1e-6)
        for t, c in self.Reynolds_number_eqn.items():
            s = iscale.get_scaling_factor(self.N_Re[t],
                                          default=1,
                                          warning=True)
            iscale.constraint_scaling_transform(c, s * 1e5, overwrite=False)
        for t, c in self.heat_flux_conv_eqn.items():
            s = iscale.get_scaling_factor(self.heat_flux_conv[t],
                                          default=1,
                                          warning=True)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
        for t, c in self.hconv_eqn.items():
            s = iscale.get_scaling_factor(self.hconv[t],
                                          default=1,
                                          warning=True)
            s *= iscale.get_scaling_factor(self.diameter_in,
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
        for t, c in self.pressure_change_eqn.items():
            s = iscale.get_scaling_factor(self.deltaP[t],
                                          default=1,
                                          warning=True)
            s *= iscale.get_scaling_factor(self.diameter_in,
                                           default=1,
                                           warning=True)
            iscale.constraint_scaling_transform(c, s, overwrite=False)
コード例 #16
0
ファイル: watertank.py プロジェクト: xiangyuy/idaes-pse
class WaterTankData(UnitModelBlockData):
    """
    Water Tank Unit Operation Class
    """
    CONFIG = UnitModelBlockData.CONFIG()

    CONFIG.declare("tank_type", ConfigValue(
        default="simple_tank",
        domain=In(["simple_tank", "rectangular_tank",
                   "vertical_cylindrical_tank",
                   "horizontal_cylindrical_tank"]),
        description="Flag indicating the tank type",
        doc="""Flag indicating the type of tank to be modeled, and
then calculate the volume of the filled level consequently,
**default** - simple_tank.
**Valid values:** {
**simple_tank** - use a general tank and provide the area,
**rectangular_tank** - use a rectangular tank and provide the width and length,
**vertical_cylindrical_tank** - use a vertical cylindrical tank
and provide the diameter,
**horizontal_cylindrical_tank** - use a horizontal cylindrical tank and
provide the length and diameter.}"""))
    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("has_pressure_change", ConfigValue(
        default=True,
        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,
**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()

        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 Inlet and Outlet 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)

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

        # Set Unit Geometry and Holdup Volume
        self._set_geometry()

        # Construct performance equations
        self._make_performance()

    def _set_geometry(self):
        """
        Define the geometry of the unit as necessary
        """
        if self.config.tank_type == "simple_tank":
            # Declare a variable for cross sectional area
            self.tank_cross_sect_area = Var(initialize=1.0,
                                            doc="Cross-sectional"
                                            " area of the tank")

        elif self.config.tank_type == "rectangular_tank":
            # Declare variables for width and length
            self.tank_width = Var(initialize=1.0,
                                  doc="Width of the tank")
            self.tank_length = Var(initialize=1.0,
                                   doc="Length of the tank")

        elif self.config.tank_type == "horizontal_cylindrical_tank" or \
                                      "vertical_cylindrical_tank":
            # Declare a variable for diameter of the tank
            self.tank_diameter = Var(initialize=0.5,
                                     doc="Inside diameter of the tank")
            if self.config.tank_type == "horizontal_cylindrical_tank":
                # Declare a variable for length of the tank
                self.tank_length = Var(initialize=1,
                                       doc="Length of the tank")

    def _make_performance(self):
        """
        Define constraints which describe the behaviour of the unit model
        """

        # Add performance variables
        self.tank_level = Var(self.flowsheet().time,
                              initialize=1.0,
                              doc="Water level from in the tank")

        # Auxiliar expressions for volume
        # Rectangular tank
        if self.config.tank_type == "rectangular_tank":
            # Calculation of cross-sectional area of the rectangle
            @self.Expression(doc="Cross-sectional area of the tank")
            def tank_cross_sect_area(b):
                return b.tank_width * b.tank_length

        # Vertical cylindrical tank
        elif self.config.tank_type == "vertical_cylindrical_tank":
            @self.Expression(doc="Radius of the tank")
            def tank_radius(b):
                return b.tank_diameter/2
            # Calculation of cross-sectional area of the vertical cylinder
            @self.Expression(doc="Cross-sectional area of the tank")
            def tank_cross_sect_area(b):
                return const.pi * b.tank_radius**2

        # Horizontal cylindrical tank
        elif self.config.tank_type == "horizontal_cylindrical_tank":
            # Calculation of area covered by the liquid level
            # at one end of the tank
            @self.Expression(doc="Radius of the tank")
            def tank_radius(b):
                return b.tank_diameter/2
            # Angle of the circular sector used to calculate the area

            @self.Expression(self.flowsheet().time,
                             doc="Angle of the circular"
                             " sector of liquid level")
            def alpha_tank(b, t):
                return 2*acos((b.tank_radius-b.tank_level[t])/b.tank_radius)

            @self.Expression(self.flowsheet().time,
                             doc="Area covered by the liquid level"
                             " at one end of the tank")
            def tank_area(b, t):
                return 0.5*b.alpha_tank[t]*b.tank_radius**2 \
                    - (b.tank_radius - b.tank_level[t]) \
                    * (2*b.tank_radius * b.tank_level[t]
                       - b.tank_level[t]**2)**0.5

        # Constraint for volume of the liquid in tank
        @self.Constraint(self.flowsheet().time,
                         doc="volume of liquid in the tank")
        def volume_eqn(b, t):
            if self.config.tank_type == "horizontal_cylindrical_tank":
                return b.volume[t] == b.tank_length * b.tank_area[t]
            else:
                return b.volume[t] == b.tank_level[t]*b.tank_cross_sect_area

        # Pressure change equation due gravity
        @self.Constraint(self.flowsheet().time, doc="pressure drop")
        def pressure_change_eqn(b, t):
            return b.deltaP[t] == \
                b.control_volume.properties_in[t].dens_mass_phase["Liq"] * \
                const.acceleration_gravity * b.tank_level[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):
        '''
        Water tank 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

                     * 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=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(state_args=state_args,
                                              outlvl=outlvl,
                                              optarg=optarg,
                                              solver=solver)
        init_log.info_high("Initialization Step 1 Complete.")

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

        # solve model
        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 pressure
        for t in blk.flowsheet().time:
            blk.control_volume.properties_out[t].pressure.unfix()
        blk.pressure_change_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)
        init_log.info("Initialization Complete.")

    def calculate_scaling_factors(self):
        pass
コード例 #17
0
class HeatExchanger1DData(UnitModelBlockData):
    """Standard Heat Exchanger 1D Unit Model Class."""

    CONFIG = UnitModelBlockData.CONFIG()
    # Template for config arguments for shell and tube side
    _SideTemplate = ConfigBlock()
    _SideTemplate.declare(
        "dynamic",
        ConfigValue(
            default=useDefault,
            domain=DefaultBool,
            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.}""",
        ),
    )
    _SideTemplate.declare(
        "has_holdup",
        ConfigValue(
            default=useDefault,
            domain=DefaultBool,
            description="Holdup construction flag",
            doc="""Indicates whether holdup terms should be constructed or not.
Must be True if dynamic = True,
**default** - False.
**Valid values:** {
**useDefault** - get flag from parent (default = False),
**True** - construct holdup terms,
**False** - do not construct holdup terms}""",
        ),
    )
    _SideTemplate.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.}""",
        ),
    )
    _SideTemplate.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.}""",
        ),
    )
    _SideTemplate.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.}""",
        ),
    )
    _SideTemplate.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.}""",
        ),
    )
    _SideTemplate.declare(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=Bool,
            description="Phase equilibrium term construction flag",
            doc="""Argument to enable phase equilibrium on the shell side.
- True - include phase equilibrium term
- False - do not include phase equilibrium term""",
        ),
    )
    _SideTemplate.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""",
        ),
    )
    _SideTemplate.declare(
        "property_package_args",
        ConfigValue(
            default={},
            description="Arguments for constructing shell 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)""",
        ),
    )
    # TODO : We should probably think about adding a consistency check for the
    # TODO : discretisation methods as well.
    _SideTemplate.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.""",
        ),
    )
    _SideTemplate.declare(
        "transformation_scheme",
        ConfigValue(
            default=useDefault,
            description="Discretization scheme to use for DAE transformation",
            doc="""Discretization scheme to use when transformating domain. See
Pyomo documentation for supported schemes.""",
        ),
    )

    # Create individual config blocks for shell and tube side
    CONFIG.declare(
        "shell_side", _SideTemplate(doc="shell side config arguments"))
    CONFIG.declare(
        "tube_side", _SideTemplate(doc="tube side config arguments"))

    # Common config args for both sides
    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(
        "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=3)""",
        ),
    )
    CONFIG.declare(
        "flow_type",
        ConfigValue(
            default=HeatExchangerFlowPattern.cocurrent,
            domain=In(HeatExchangerFlowPattern),
            description="Flow configuration of heat exchanger",
            doc="""Flow configuration of heat exchanger
- HeatExchangerFlowPattern.cocurrent: shell and tube flows from 0 to 1
(default)
- HeatExchangerFlowPattern.countercurrent: shell side flows from 0 to 1
tube side flows from 1 to 0""",
        ),
    )
    CONFIG.declare(
        "has_wall_conduction",
        ConfigValue(
            default=WallConductionType.zero_dimensional,
            domain=In(WallConductionType),
            description="Conduction model for tube wall",
            doc="""Argument to enable type of wall heat conduction model.
- WallConductionType.zero_dimensional - 0D wall model (default),
- WallConductionType.one_dimensional - 1D wall model along the thickness of the
tube,
- WallConductionType.two_dimensional - 2D wall model along the lenghth and
thickness of the tube""",
        ),
    )

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

        Args:
            None

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

        # Set flow directions for the control volume blocks and specify
        # dicretisation if not specified.
        if self.config.flow_type == HeatExchangerFlowPattern.cocurrent:
            set_direction_shell = FlowDirection.forward
            set_direction_tube = FlowDirection.forward
            if (
                self.config.shell_side.transformation_method
                != self.config.tube_side.transformation_method
            ) or (
                self.config.shell_side.transformation_scheme
                != self.config.tube_side.transformation_scheme
            ):
                raise ConfigurationError(
                    "HeatExchanger1D only supports similar transformation "
                    "schemes on the shell side and tube side domains for "
                    "both cocurrent and countercurrent flow patterns."
                )
            if self.config.shell_side.transformation_method is useDefault:
                _log.warning(
                    "Discretization method was "
                    "not specified for the shell side of the "
                    "co-current heat exchanger. "
                    "Defaulting to finite "
                    "difference method on the shell side."
                )
                self.config.shell_side.transformation_method = \
                    "dae.finite_difference"
            if self.config.tube_side.transformation_method is useDefault:
                _log.warning(
                    "Discretization method was "
                    "not specified for the tube side of the "
                    "co-current heat exchanger. "
                    "Defaulting to finite "
                    "difference method on the tube side."
                )
                self.config.tube_side.transformation_method = \
                    "dae.finite_difference"
            if self.config.shell_side.transformation_scheme is useDefault:
                _log.warning(
                    "Discretization scheme was "
                    "not specified for the shell side of the "
                    "co-current heat exchanger. "
                    "Defaulting to backward finite "
                    "difference on the shell side."
                )
                self.config.shell_side.transformation_scheme = "BACKWARD"
            if self.config.tube_side.transformation_scheme is useDefault:
                _log.warning(
                    "Discretization scheme was "
                    "not specified for the tube side of the "
                    "co-current heat exchanger. "
                    "Defaulting to backward finite "
                    "difference on the tube side."
                )
                self.config.tube_side.transformation_scheme = "BACKWARD"
        elif self.config.flow_type == HeatExchangerFlowPattern.countercurrent:
            set_direction_shell = FlowDirection.forward
            set_direction_tube = FlowDirection.backward
            if self.config.shell_side.transformation_method is useDefault:
                _log.warning(
                    "Discretization method was "
                    "not specified for the shell side of the "
                    "counter-current heat exchanger. "
                    "Defaulting to finite "
                    "difference method on the shell side."
                )
                self.config.shell_side.transformation_method = \
                    "dae.finite_difference"
            if self.config.tube_side.transformation_method is useDefault:
                _log.warning(
                    "Discretization method was "
                    "not specified for the tube side of the "
                    "counter-current heat exchanger. "
                    "Defaulting to finite "
                    "difference method on the tube side."
                )
                self.config.tube_side.transformation_method = \
                    "dae.finite_difference"
            if self.config.shell_side.transformation_scheme is useDefault:
                _log.warning(
                    "Discretization scheme was "
                    "not specified for the shell side of the "
                    "counter-current heat exchanger. "
                    "Defaulting to backward finite "
                    "difference on the shell side."
                )
                self.config.shell_side.transformation_scheme = "BACKWARD"
            if self.config.tube_side.transformation_scheme is useDefault:
                _log.warning(
                    "Discretization scheme was "
                    "not specified for the tube side of the "
                    "counter-current heat exchanger. "
                    "Defaulting to forward finite "
                    "difference on the tube side."
                )
                self.config.tube_side.transformation_scheme = "BACKWARD"
        else:
            raise ConfigurationError(
                "{} HeatExchanger1D only supports cocurrent and "
                "countercurrent flow patterns, but flow_type configuration"
                " argument was set to {}."
                .format(self.name, self.config.flow_type)
            )

        # Control volume 1D for shell
        self.shell = ControlVolume1DBlock(
            default={
                "dynamic": self.config.shell_side.dynamic,
                "has_holdup": self.config.shell_side.has_holdup,
                "property_package": self.config.shell_side.property_package,
                "property_package_args":
                self.config.shell_side.property_package_args,
                "transformation_method":
                self.config.shell_side.transformation_method,
                "transformation_scheme":
                self.config.shell_side.transformation_scheme,
                "finite_elements": self.config.finite_elements,
                "collocation_points": self.config.collocation_points,
            }
        )

        self.tube = ControlVolume1DBlock(
            default={
                "dynamic": self.config.tube_side.dynamic,
                "has_holdup": self.config.tube_side.has_holdup,
                "property_package": self.config.tube_side.property_package,
                "property_package_args":
                self.config.tube_side.property_package_args,
                "transformation_method":
                self.config.tube_side.transformation_method,
                "transformation_scheme":
                self.config.tube_side.transformation_scheme,
                "finite_elements": self.config.finite_elements,
                "collocation_points": self.config.collocation_points,
            }
        )

        self.shell.add_geometry(flow_direction=set_direction_shell)
        self.tube.add_geometry(flow_direction=set_direction_tube)

        self.shell.add_state_blocks(
            information_flow=set_direction_shell,
            has_phase_equilibrium=self.config.shell_side.has_phase_equilibrium,
        )
        self.tube.add_state_blocks(
            information_flow=set_direction_tube,
            has_phase_equilibrium=self.config.tube_side.has_phase_equilibrium,
        )

        # Populate shell
        self.shell.add_material_balances(
            balance_type=self.config.shell_side.material_balance_type,
            has_phase_equilibrium=self.config.shell_side.has_phase_equilibrium,
        )

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

        self.shell.add_momentum_balances(
            balance_type=self.config.shell_side.momentum_balance_type,
            has_pressure_change=self.config.shell_side.has_pressure_change,
        )

        self.shell.apply_transformation()

        # Populate tube
        self.tube.add_material_balances(
            balance_type=self.config.tube_side.material_balance_type,
            has_phase_equilibrium=self.config.tube_side.has_phase_equilibrium,
        )

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

        self.tube.add_momentum_balances(
            balance_type=self.config.tube_side.momentum_balance_type,
            has_pressure_change=self.config.tube_side.has_pressure_change,
        )

        self.tube.apply_transformation()

        # Add Ports for shell side
        self.add_inlet_port(name="shell_inlet", block=self.shell)
        self.add_outlet_port(name="shell_outlet", block=self.shell)

        # Add Ports for tube side
        self.add_inlet_port(name="tube_inlet", block=self.tube)
        self.add_outlet_port(name="tube_outlet", block=self.tube)

        # Add reference to control volume geometry
        add_object_reference(self, "shell_area", self.shell.area)
        add_object_reference(self, "shell_length", self.shell.length)
        add_object_reference(self, "tube_area", self.tube.area)
        add_object_reference(self, "tube_length", self.tube.length)

        self._make_performance()

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

        Args:
            None

        Returns:
            None
        """
        shell_units = self.config.shell_side.property_package.\
            get_metadata().get_derived_units
        tube_units = self.config.tube_side.property_package.\
            get_metadata().get_derived_units

        # Unit model variables
        # HX dimensions
        self.d_shell = Var(initialize=1,
                           doc="Diameter of shell",
                           units=shell_units("length"))
        self.d_tube_outer = Var(initialize=0.011,
                                doc="Outer diameter of tube",
                           units=shell_units("length"))
        self.d_tube_inner = Var(initialize=0.010,
                                doc="Inner diameter of tube",
                           units=shell_units("length"))
        self.N_tubes = Var(initialize=1,
                           doc="Number of tubes",
                           units=pyunits.dimensionless)

        # Note: In addition to the above variables, "shell_length" and
        # "tube_length" need to be fixed at the flowsheet level

        # Performance variables
        self.shell_heat_transfer_coefficient = Var(
            self.flowsheet().time,
            self.shell.length_domain,
            initialize=50,
            doc="Heat transfer coefficient",
            units=shell_units("heat_transfer_coefficient")
        )
        self.tube_heat_transfer_coefficient = Var(
            self.flowsheet().time,
            self.tube.length_domain,
            initialize=50,
            doc="Heat transfer coefficient",
            units=tube_units("heat_transfer_coefficient")
        )

        # Wall 0D model (Q_shell = Q_tube*N_tubes)
        if self.config.has_wall_conduction == \
                WallConductionType.zero_dimensional:
            self.temperature_wall = Var(
                self.flowsheet().time,
                self.tube.length_domain,
                initialize=298.15,
                units=shell_units("temperature")
            )

            # Performance equations
            # Energy transfer between shell and tube wall

            @self.Constraint(
                self.flowsheet().time,
                self.shell.length_domain,
                doc="Heat transfer between shell and tube",
            )
            def shell_heat_transfer_eq(self, t, x):
                return self.shell.heat[t, x] == -self.N_tubes * (
                    self.shell_heat_transfer_coefficient[t, x]
                    * c.pi
                    * self.d_tube_outer
                    * (
                        self.shell.properties[t, x].temperature
                        - self.temperature_wall[t, x]
                    )
                )

            # Energy transfer between tube wall and tube
            @self.Constraint(
                self.flowsheet().time,
                self.tube.length_domain,
                doc="Convective heat transfer",
            )
            def tube_heat_transfer_eq(self, t, x):
                return self.tube.heat[t, x] == \
                    self.tube_heat_transfer_coefficient[
                    t, x
                ] * c.pi * pyunits.convert(self.d_tube_inner,
                                           to_units=tube_units("length")) * (
                    pyunits.convert(self.temperature_wall[t, x],
                                    to_units=tube_units('temperature')) -
                    self.tube.properties[t, x].temperature
                )

            if shell_units("length") is None:
                # Backwards compatability check
                q_units = None
            else:
                q_units = shell_units("power")/shell_units("length")
            # Wall 0D model
            @self.Constraint(
                self.flowsheet().time,
                self.shell.length_domain,
                doc="wall 0D model",
            )
            def wall_0D_model(self, t, x):
                return pyunits.convert(self.tube.heat[t, x],
                                       to_units=q_units) == -(
                            self.shell.heat[t, x] / self.N_tubes)

        else:
            raise NotImplementedError(
                "{} HeatExchanger1D has not yet implemented support for "
                "wall conduction models."
            )

        # Define tube area in terms of tube diameter
        self.area_calc_tube = Constraint(
            expr=4 * self.tube_area == c.pi * pyunits.convert(
                self.d_tube_inner, to_units=tube_units("length"))**2
        )

        # Define shell area in terms of shell and tube diameter
        self.area_calc_shell = Constraint(
            expr=4 * self.shell_area
            == c.pi * (self.d_shell**2 - self.N_tubes*self.d_tube_outer**2)
        )

    def initialize(
        self,
        shell_state_args=None,
        tube_state_args=None,
        outlvl=idaeslog.NOTSET,
        solver=None,
        optarg=None,
    ):
        """
        Initialization routine for the unit.

        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, use
                     default solver options)
            solver : str indicating which 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)

        # ---------------------------------------------------------------------
        # Initialize shell block
        flags_shell = self.shell.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=shell_state_args,
        )

        flags_tube = self.tube.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=tube_state_args,
        )

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

        # ---------------------------------------------------------------------
        # Solve unit
        # Wall 0D
        if self.config.has_wall_conduction == \
            WallConductionType.zero_dimensional:
            shell_units = self.config.shell_side.property_package.\
                get_metadata().get_derived_units
            for t in self.flowsheet().time:
                for z in self.shell.length_domain:
                    self.temperature_wall[t, z].fix(
                        value(
                            0.5
                            * (
                                self.shell.properties[t, 0].temperature
                                + pyunits.convert(
                                    self.tube.properties[t, 0].temperature,
                                    to_units=shell_units('temperature'))
                            )
                        )
                    )

            self.tube.deactivate()
            self.tube_heat_transfer_eq.deactivate()
            self.wall_0D_model.deactivate()

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

            self.tube.activate()
            self.tube_heat_transfer_eq.activate()

            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.wall_0D_model.activate()
            self.temperature_wall.unfix()

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

        self.shell.release_state(flags_shell)
        self.tube.release_state(flags_tube)

        init_log.info("Initialization Complete.")

    def _get_performance_contents(self, time_point=0):
        var_dict = {}
        var_dict["Shell Area"] = self.shell.area
        var_dict["Shell Diameter"] = self.d_shell
        var_dict["Shell Length"] = self.shell.length
        var_dict["Tube Area"] = self.tube.area
        var_dict["Tube Outer Diameter"] = self.d_tube_outer
        var_dict["Tube Inner Diameter"] = self.d_tube_inner
        var_dict["Tube Length"] = self.tube.length
        var_dict["Number of Tubes"] = self.N_tubes

        return {"vars": var_dict}

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(
            {
                "Shell Inlet": self.shell_inlet,
                "Shell Outlet": self.shell_outlet,
                "Tube Inlet": self.tube_inlet,
                "Tube Outlet": self.tube_outlet,
            },
            time_point=time_point,
        )

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

        for i, c in self.shell_heat_transfer_eq.items():
            iscale.constraint_scaling_transform(
                c, iscale.get_scaling_factor(
                    self.shell.heat[i], default=1, warning=True),
                overwrite=False)

        for i, c in self.tube_heat_transfer_eq.items():
            iscale.constraint_scaling_transform(c, iscale.get_scaling_factor(
                    self.tube.heat[i], default=1, warning=True),
                overwrite=False)
コード例 #18
0
ファイル: solvent_column.py プロジェクト: jmorgan29/idaes-pse
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()
コード例 #19
0
class HeatExchangerData(UnitModelBlockData):
    """
    Simple 0D heat exchange unit.
    Unit model to transfer heat from one material to another.
    """

    CONFIG = UnitModelBlockData.CONFIG(implicit=True)
    _make_heat_exchanger_config(CONFIG)

    def _process_config(self):
        """Check for configuration errors and alternate config option names.
        """
        config = self.config

        if config.hot_side_name == config.cold_side_name:
            raise NameError(
                "Heatexchanger hot and cold side cannot have the same name '{}'."
                " Be sure to set both the hot_side_name and cold_side_name.".
                format(config.hot_side_name))

        for o in config:
            if not (o in self.CONFIG
                    or o in [config.hot_side_name, config.cold_side_name]):
                raise KeyError(
                    "Heatexchanger config option {} not defined".format(o))

        if config.hot_side_name in config:
            config.hot_side_config.set_value(config[config.hot_side_name])
            # Allow access to hot_side_config under the hot_side_name, backward
            # compatible with the tube and shell notation
            setattr(config, config.hot_side_name, config.hot_side_config)
        if config.cold_side_name in config:
            config.cold_side_config.set_value(config[config.cold_side_name])
            # Allow access to hot_side_config under the cold_side_name, backward
            # compatible with the tube and shell notation
            setattr(config, config.cold_side_name, config.cold_side_config)

        if config.cold_side_name in ["hot_side", "side_1"]:
            raise ConfigurationError(
                "Cold side name cannot be in ['hot_side', 'side_1'].")
        if config.hot_side_name in ["cold_side", "side_2"]:
            raise ConfigurationError(
                "Hot side name cannot be in ['cold_side', 'side_2'].")

    def build(self):
        """
        Building model

        Args:
            None
        Returns:
            None
        """
        ########################################################################
        #  Call UnitModel.build to setup dynamics and configure                #
        ########################################################################
        super().build()
        self._process_config()
        config = self.config

        ########################################################################
        # Add control volumes                                                  #
        ########################################################################
        hot_side = _make_heater_control_volume(
            self,
            config.hot_side_name,
            config.hot_side_config,
            dynamic=config.dynamic,
            has_holdup=config.has_holdup,
        )
        cold_side = _make_heater_control_volume(
            self,
            config.cold_side_name,
            config.cold_side_config,
            dynamic=config.dynamic,
            has_holdup=config.has_holdup,
        )
        # Add references to the hot side and cold side, so that we have solid
        # names to refer to internally.  side_1 and side_2 also maintain
        # compatability with older models.  Using add_object_reference keeps
        # these from showing up when you iterate through pyomo compoents in a
        # model, so only the user specified control volume names are "seen"
        if not hasattr(self, "side_1"):
            add_object_reference(self, "side_1", hot_side)
        if not hasattr(self, "side_2"):
            add_object_reference(self, "side_2", cold_side)
        if not hasattr(self, "hot_side"):
            add_object_reference(self, "hot_side", hot_side)
        if not hasattr(self, "cold_side"):
            add_object_reference(self, "cold_side", cold_side)

        ########################################################################
        # Add variables                                                        #
        ########################################################################
        # Use hot side units as basis
        s1_metadata = config.hot_side_config.property_package.get_metadata()

        q_units = s1_metadata.get_derived_units("power")
        u_units = s1_metadata.get_derived_units("heat_transfer_coefficient")
        a_units = s1_metadata.get_derived_units("area")
        temp_units = s1_metadata.get_derived_units("temperature")

        u = self.overall_heat_transfer_coefficient = Var(
            self.flowsheet().config.time,
            domain=PositiveReals,
            initialize=100.0,
            doc="Overall heat transfer coefficient",
            units=u_units)
        a = self.area = Var(domain=PositiveReals,
                            initialize=1000.0,
                            doc="Heat exchange area",
                            units=a_units)
        self.delta_temperature_in = Var(
            self.flowsheet().config.time,
            initialize=10.0,
            doc="Temperature difference at the hot inlet end",
            units=temp_units)
        self.delta_temperature_out = Var(
            self.flowsheet().config.time,
            initialize=10.1,
            doc="Temperature difference at the hot outlet end",
            units=temp_units)
        if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
            self.crossflow_factor = Var(
                self.flowsheet().config.time,
                initialize=1.0,
                doc="Factor to adjust coutercurrent flow heat "
                "transfer calculation for cross flow.",
            )
            f = self.crossflow_factor

        self.heat_duty = Reference(cold_side.heat)
        ########################################################################
        # Add ports                                                            #
        ########################################################################
        i1 = self.add_inlet_port(name=f"{config.hot_side_name}_inlet",
                                 block=hot_side,
                                 doc="Hot side inlet")
        i2 = self.add_inlet_port(name=f"{config.cold_side_name}_inlet",
                                 block=cold_side,
                                 doc="Cold side inlet")
        o1 = self.add_outlet_port(name=f"{config.hot_side_name}_outlet",
                                  block=hot_side,
                                  doc="Hot side outlet")
        o2 = self.add_outlet_port(name=f"{config.cold_side_name}_outlet",
                                  block=cold_side,
                                  doc="Cold side outlet")

        # Using Andrew's function for now.  I want these port names for backward
        # compatablity, but I don't want them to appear if you iterate throught
        # components and add_object_reference hides them from Pyomo.
        if not hasattr(self, "inlet_1"):
            add_object_reference(self, "inlet_1", i1)
        if not hasattr(self, "inlet_2"):
            add_object_reference(self, "inlet_2", i2)
        if not hasattr(self, "outlet_1"):
            add_object_reference(self, "outlet_1", o1)
        if not hasattr(self, "outlet_2"):
            add_object_reference(self, "outlet_2", o2)

        if not hasattr(self, "hot_inlet"):
            add_object_reference(self, "hot_inlet", i1)
        if not hasattr(self, "cold_inlet"):
            add_object_reference(self, "cold_inlet", i2)
        if not hasattr(self, "hot_outlet"):
            add_object_reference(self, "hot_outlet", o1)
        if not hasattr(self, "cold_outlet"):
            add_object_reference(self, "cold_outlet", o2)
        ########################################################################
        # Add end temperature differnece constraints                           #
        ########################################################################

        @self.Constraint(self.flowsheet().config.time)
        def delta_temperature_in_equation(b, t):
            if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent:
                return (b.delta_temperature_in[t] ==
                        hot_side.properties_in[t].temperature -
                        pyunits.convert(cold_side.properties_in[t].temperature,
                                        to_units=temp_units))
            else:
                return (
                    b.delta_temperature_in[t] ==
                    hot_side.properties_in[t].temperature -
                    pyunits.convert(cold_side.properties_out[t].temperature,
                                    to_units=temp_units))

        @self.Constraint(self.flowsheet().config.time)
        def delta_temperature_out_equation(b, t):
            if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent:
                return (
                    b.delta_temperature_out[t] ==
                    hot_side.properties_out[t].temperature -
                    pyunits.convert(cold_side.properties_out[t].temperature,
                                    to_units=temp_units))
            else:
                return (b.delta_temperature_out[t] ==
                        hot_side.properties_out[t].temperature -
                        pyunits.convert(cold_side.properties_in[t].temperature,
                                        to_units=temp_units))

        ########################################################################
        # Add a unit level energy balance                                      #
        ########################################################################
        @self.Constraint(self.flowsheet().config.time)
        def unit_heat_balance(b, t):
            return 0 == (hot_side.heat[t] +
                         pyunits.convert(cold_side.heat[t], to_units=q_units))

        ########################################################################
        # Add delta T calculations using callack function, lots of options,    #
        #   and users can provide their own if needed                          #
        ########################################################################
        config.delta_temperature_callback(self)
        ########################################################################
        # Add Heat transfer equation                                           #
        ########################################################################
        deltaT = self.delta_temperature

        @self.Constraint(self.flowsheet().config.time)
        def heat_transfer_equation(b, t):
            if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
                return pyunits.convert(self.heat_duty[t],
                                       to_units=q_units) == (f[t] * u[t] * a *
                                                             deltaT[t])
            else:
                return pyunits.convert(self.heat_duty[t],
                                       to_units=q_units) == (u[t] * a *
                                                             deltaT[t])

        ########################################################################
        # Add symbols for LaTeX equation rendering                             #
        ########################################################################
        self.overall_heat_transfer_coefficient.latex_symbol = "U"
        self.area.latex_symbol = "A"
        hot_side.heat.latex_symbol = "Q_1"
        cold_side.heat.latex_symbol = "Q_2"
        self.delta_temperature.latex_symbol = "\\Delta T"

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

        Args:
            state_args_1 : a dict of arguments to be passed to the property
                initialization for the hot side (see documentation of the specific
                property package) (default = {}).
            state_args_2 : a dict of arguments to be passed to the property
                initialization for the cold side (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')
            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 = getattr(self, self.config.hot_side_name)
        cold_side = getattr(self, self.config.cold_side_name)

        opt = SolverFactory(solver)
        opt.options = optarg
        flags1 = hot_side.initialize(outlvl=outlvl,
                                     optarg=optarg,
                                     solver=solver,
                                     state_args=state_args_1)

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

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

        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.heat_transfer_equation.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.heat_transfer_equation.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_performance_contents(self, time_point=0):
        var_dict = {
            "HX Coefficient":
            self.overall_heat_transfer_coefficient[time_point]
        }
        var_dict["HX Area"] = self.area
        var_dict["Heat Duty"] = self.heat_duty[time_point]
        if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
            var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]}

        expr_dict = {}
        expr_dict["Delta T Driving"] = self.delta_temperature[time_point]
        expr_dict["Delta T In"] = self.delta_temperature_in[time_point]
        expr_dict["Delta T Out"] = self.delta_temperature_out[time_point]

        return {"vars": var_dict, "exprs": expr_dict}

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(
            {
                "Hot Inlet": self.inlet_1,
                "Hot Outlet": self.outlet_1,
                "Cold Inlet": self.inlet_2,
                "Cold Outlet": self.outlet_2,
            },
            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)

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

        # We have a pretty good idea that the delta Ts will be between about
        # 1 and 100 regardless of process of temperature units, so a default
        # should be fine, so don't warn.  Guessing a typical delta t around 10
        # the default scaling factor is set to 0.1
        sf_dT1 = dict(
            zip(self.delta_temperature_in.keys(), [
                iscale.get_scaling_factor(v, default=0.1)
                for v in self.delta_temperature_in.values()
            ]))
        sf_dT2 = dict(
            zip(self.delta_temperature_out.keys(), [
                iscale.get_scaling_factor(v, default=0.1)
                for v in self.delta_temperature_out.values()
            ]))

        # U depends a lot on the process and units of measure so user should set
        # this one.
        sf_u = dict(
            zip(self.overall_heat_transfer_coefficient.keys(), [
                iscale.get_scaling_factor(v, default=1, warning=True)
                for v in self.overall_heat_transfer_coefficient.values()
            ]))

        # Since this depends on the process size this is another scaling factor
        # the user should always set.
        sf_a = iscale.get_scaling_factor(self.area, default=1, warning=True)

        for t, c in self.heat_transfer_equation.items():
            iscale.constraint_scaling_transform(c, sf_dT1[t] * sf_u[t] * sf_a)

        for t, c in self.unit_heat_balance.items():
            iscale.constraint_scaling_transform(c, sf_dT1[t] * sf_u[t] * sf_a)

        for t, c in self.delta_temperature_in_equation.items():
            iscale.constraint_scaling_transform(c, sf_dT1[t])

        for t, c in self.delta_temperature_out_equation.items():
            iscale.constraint_scaling_transform(c, sf_dT2[t])

        if hasattr(self, "costing"):
            # import costing scaling factors
            costing.calculate_scaling_factors(self.costing)
コード例 #20
0
class FWH0DData(UnitModelBlockData):
    CONFIG = UnitModelBlockData.CONFIG()
    _define_feedwater_heater_0D_config(CONFIG)

    def build(self):
        super().build()
        config = self.config  # sorter ref to config for less line splitting

        # All feedwater heaters have a condensing section
        _set_prop_pack(config.condense, config)
        self.condense = FWHCondensing0D(default=config.condense)

        # Add a mixer to add the drain stream from another feedwater heater
        if config.has_drain_mixer:
            mix_cfg = {  # general unit model config
                "dynamic": config.dynamic,
                "has_holdup": config.has_holdup,
                "property_package": config.property_package,
                "property_package_args": config.property_package_args,
                "momentum_mixing_type": MomentumMixingType.none,
                "material_balance_type": MaterialBalanceType.componentTotal,
                "inlet_list": ["steam", "drain"],
            }
            self.drain_mix = Mixer(default=mix_cfg)

            @self.drain_mix.Constraint(self.drain_mix.flowsheet().config.time)
            def mixer_pressure_constraint(b, t):
                """
                Constraint to set the drain mixer pressure to the pressure of
                the steam extracted from the turbine. The drain inlet should
                always be a higher pressure than the steam inlet.
                """
                return b.steam_state[t].pressure == b.mixed_state[t].pressure

            # Connect the mixer to the condensing section inlet
            self.mix_out_arc = Arc(source=self.drain_mix.outlet,
                                   destination=self.condense.inlet_1)

        # Add a desuperheat section before the condensing section
        if config.has_desuperheat:
            _set_prop_pack(config.desuperheat, config)
            self.desuperheat = HeatExchanger(default=config.desuperheat)
            # set default area less than condensing section area, this will
            # almost always be overridden by the user fixing an area later
            self.desuperheat.area.value = 10
            if config.has_drain_mixer:
                self.desuperheat_drain_arc = Arc(
                    source=self.desuperheat.outlet_1,
                    destination=self.drain_mix.steam)
            else:
                self.desuperheat_drain_arc = Arc(
                    source=self.desuperheat.outlet_1,
                    destination=self.condense.inlet_1)
            self.condense_out2_arc = Arc(source=self.condense.outlet_2,
                                         destination=self.desuperheat.inlet_2)

        # Add a drain cooling section after the condensing section
        if config.has_drain_cooling:
            _set_prop_pack(config.cooling, config)
            self.cooling = HeatExchanger(default=config.cooling)
            # set default area less than condensing section area, this will
            # almost always be overridden by the user fixing an area later
            self.cooling.area.value = 10
            self.cooling_out2_arc = Arc(source=self.cooling.outlet_2,
                                        destination=self.condense.inlet_2)
            self.condense_out1_arc = Arc(source=self.condense.outlet_1,
                                         destination=self.cooling.inlet_1)

        TransformationFactory("network.expand_arcs").apply_to(self)

    def initialize(self, *args, **kwargs):
        outlvl = kwargs.get("outlvl", idaeslog.NOTSET)

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

        config = self.config  # shorter ref to config for less line splitting
        sp = StoreSpec.value_isfixed_isactive(only_fixed=True)
        istate = to_json(self, return_dict=True, wts=sp)

        # the initialization here isn't straight forward since the heat exchanger
        # may have 3 stages and they are countercurrent.  For simplicity each
        # stage in initialized with the same cooling water inlet conditions then
        # the whole feedwater heater is solved together.  There are more robust
        # approaches which can be implimented if the need arises.

        # initialize desuperheat if include
        if config.has_desuperheat:
            if config.has_drain_cooling:
                _set_port(self.desuperheat.inlet_2, self.cooling.inlet_2)
            else:
                _set_port(self.desuperheat.inlet_2, self.condense.inlet_2)
            self.desuperheat.initialize(*args, **kwargs)
            self.desuperheat.inlet_1.flow_mol.unfix()
            if config.has_drain_mixer:
                _set_port(self.drain_mix.steam, self.desuperheat.outlet_1)
            else:
                _set_port(self.condense.inlet_1, self.desuperheat.outlet_1)
            # fix the steam and fwh inlet for init
            self.desuperheat.inlet_1.fix()
            self.desuperheat.inlet_1.flow_mol.unfix()  # unfix for extract calc
        # initialize mixer if included
        if config.has_drain_mixer:
            self.drain_mix.steam.fix()
            self.drain_mix.drain.fix()
            self.drain_mix.outlet.unfix()
            self.drain_mix.initialize(*args, **kwargs)
            _set_port(self.condense.inlet_1, self.drain_mix.outlet)
            if config.has_desuperheat:
                self.drain_mix.steam.unfix()
            else:
                self.drain_mix.steam.flow_mol.unfix()
        # Initialize condense section
        if config.has_drain_cooling:
            _set_port(self.condense.inlet_2, self.cooling.inlet_2)
            self.cooling.inlet_2.fix()
        else:
            self.condense.inlet_2.fix()
        if not config.has_drain_mixer and not config.has_desuperheat:
            self.condense.inlet_1.fix()
            self.condense.inlet_1.flow_mol.unfix()

        tempsat = value(self.condense.shell.properties_in[0].temperature_sat)
        temp = value(self.condense.tube.properties_in[0].temperature)
        if tempsat - temp < 30:
            init_log.warning(
                "The steam sat. temperature ({}) is near the feedwater"
                " inlet temperature ({})".format(tempsat, temp))

        self.condense.initialize(*args, **kwargs)
        # Initialize drain cooling if included
        if config.has_drain_cooling:
            _set_port(self.cooling.inlet_1, self.condense.outlet_1)
            self.cooling.initialize(*args, **kwargs)
        # Solve all together
        opt = SolverFactory(kwargs.get("solver", "ipopt"))
        opt.options = kwargs.get("oparg", {})
        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("Condensing shell inlet delta T = {}".format(
            value(self.condense.delta_temperature_in[0])))
        init_log.info("Condensing Shell outlet delta T = {}".format(
            value(self.condense.delta_temperature_out[0])))
        init_log.info("Steam Flow = {}".format(
            value(self.condense.inlet_1.flow_mol[0])))
        init_log.info("Initialization Complete: {}".format(
            idaeslog.condition(res)))

        from_json(self, sd=istate, wts=sp)
コード例 #21
0
class HeatExchangerData(UnitModelBlockData):
    """
    Simple 0D heat exchange unit.
    Unit model to transfer heat from one material to another.
    """

    CONFIG = UnitModelBlockData.CONFIG(implicit=True)
    _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 _process_config(self):
        """Check for configuration errors and alternate config option names.
        """
        config = self.config

        if config.hot_side_name == config.cold_side_name:
            raise NameError(
                "Heatexchanger hot and cold side cannot have the same name '{}'."
                " Be sure to set both the hot_side_name and cold_side_name.".
                format(config.hot_side_name))

        for o in config:
            if not (o in self.CONFIG
                    or o in [config.hot_side_name, config.cold_side_name]):
                raise KeyError(
                    "Heatexchanger config option {} not defined".format(o))

        if config.hot_side_name in config:
            config.hot_side_config.set_value(config[config.hot_side_name])
            # Allow access to hot_side_config under the hot_side_name, backward
            # compatible with the tube and shell notation
            setattr(config, config.hot_side_name, config.hot_side_config)
        if config.cold_side_name in config:
            config.cold_side_config.set_value(config[config.cold_side_name])
            # Allow access to hot_side_config under the cold_side_name, backward
            # compatible with the tube and shell notation
            setattr(config, config.cold_side_name, config.cold_side_config)

    def build(self):
        """
        Building model

        Args:
            None
        Returns:
            None
        """
        ########################################################################
        #  Call UnitModel.build to setup dynamics and configure                #
        ########################################################################
        super().build()
        self._process_config()
        config = self.config

        ########################################################################
        # Add variables                                                        #
        ########################################################################
        u = self.overall_heat_transfer_coefficient = Var(
            self.flowsheet().config.time,
            domain=PositiveReals,
            initialize=100.0,
            doc="Overall heat transfer coefficient",
        )
        a = self.area = Var(domain=PositiveReals,
                            initialize=1000.0,
                            doc="Heat exchange area")
        self.delta_temperature_in = Var(
            self.flowsheet().config.time,
            initialize=10.0,
            doc="Temperature difference at the hot inlet end",
        )
        self.delta_temperature_out = Var(
            self.flowsheet().config.time,
            initialize=10.1,
            doc="Temperature difference at the hot outlet end",
        )
        if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
            self.crossflow_factor = Var(
                self.flowsheet().config.time,
                initialize=1.0,
                doc="Factor to adjust coutercurrent flow heat "
                "transfer calculation for cross flow.",
            )
            f = self.crossflow_factor
        ########################################################################
        # Add control volumes                                                  #
        ########################################################################
        _make_heater_control_volume(
            self,
            "side_1",
            config.hot_side_config,
            dynamic=config.dynamic,
            has_holdup=config.has_holdup,
        )
        _make_heater_control_volume(
            self,
            "side_2",
            config.cold_side_config,
            dynamic=config.dynamic,
            has_holdup=config.has_holdup,
        )
        # Add named references to side_1 and side_2, side 1 and 2 maintain
        # backward compatability and are names the user doesn't need to worry
        # about. The sign convention for duty is heat from side 1 to side 2 is
        # positive
        add_object_reference(self, config.hot_side_name, self.side_1)
        add_object_reference(self, config.cold_side_name, self.side_2)

        # Add convienient references to heat duty.
        q = self.heat_duty = Reference(self.side_2.heat)
        ########################################################################
        # Add ports                                                            #
        ########################################################################
        # Keep old port names, just for backward compatability
        self.add_inlet_port(name="inlet_1",
                            block=self.side_1,
                            doc="Hot side inlet")
        self.add_inlet_port(name="inlet_2",
                            block=self.side_2,
                            doc="Cold side inlet")
        self.add_outlet_port(name="outlet_1",
                             block=self.side_1,
                             doc="Hot side outlet")
        self.add_outlet_port(name="outlet_2",
                             block=self.side_2,
                             doc="Cold side outlet")

        # Using Andrew's function for now, I think Pyomo's refrence has trouble
        # with scalar (pyomo) components.
        add_object_reference(self, config.hot_side_name + "_inlet",
                             self.inlet_1)
        add_object_reference(self, config.cold_side_name + "_inlet",
                             self.inlet_2)
        add_object_reference(self, config.hot_side_name + "_outlet",
                             self.outlet_1)
        add_object_reference(self, config.cold_side_name + "_outlet",
                             self.outlet_2)
        ########################################################################
        # Add end temperature differnece constraints                            #
        ########################################################################
        @self.Constraint(self.flowsheet().config.time)
        def delta_temperature_in_equation(b, t):
            if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent:
                return (b.delta_temperature_in[t] ==
                        b.side_1.properties_in[t].temperature -
                        b.side_2.properties_in[t].temperature)
            else:
                return (b.delta_temperature_in[t] ==
                        b.side_1.properties_in[t].temperature -
                        b.side_2.properties_out[t].temperature)

        @self.Constraint(self.flowsheet().config.time)
        def delta_temperature_out_equation(b, t):
            if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent:
                return (b.delta_temperature_out[t] ==
                        b.side_1.properties_out[t].temperature -
                        b.side_2.properties_out[t].temperature)
            else:
                return (b.delta_temperature_out[t] ==
                        b.side_1.properties_out[t].temperature -
                        b.side_2.properties_in[t].temperature)

        ########################################################################
        # Add a unit level energy balance                                      #
        ########################################################################
        @self.Constraint(self.flowsheet().config.time)
        def unit_heat_balance(b, t):
            return 0 == self.side_1.heat[t] + self.side_2.heat[t]

        ########################################################################
        # Add delta T calculations using callack function, lots of options,    #
        #   and users can provide their own if needed                          #
        ########################################################################
        config.delta_temperature_callback(self)
        ########################################################################
        # Add Heat transfer equation                                           #
        ########################################################################
        deltaT = self.delta_temperature
        scale = self.side_1.scaling_factor_energy

        @self.Constraint(self.flowsheet().config.time)
        def heat_transfer_equation(b, t):
            if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
                return 0 == (f[t] * u[t] * a * deltaT[t] - q[t]) * scale
            else:
                return 0 == (u[t] * a * deltaT[t] - q[t]) * scale

        ########################################################################
        # Add symbols for LaTeX equation rendering                             #
        ########################################################################
        self.overall_heat_transfer_coefficient.latex_symbol = "U"
        self.area.latex_symbol = "A"
        self.side_1.heat.latex_symbol = "Q_1"
        self.side_2.heat.latex_symbol = "Q_2"
        self.delta_temperature.latex_symbol = "\\Delta T"

    def initialize(
        self,
        state_args_1=None,
        state_args_2=None,
        outlvl=idaeslog.NOTSET,
        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 initialization routine
            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
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        opt = SolverFactory(solver)
        opt.options = optarg
        flags1 = self.side_1.initialize(outlvl=outlvl,
                                        optarg=optarg,
                                        solver=solver,
                                        state_args=state_args_1)

        init_log.info_high("Initialization Step 1a (side_1) Complete.")

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

        init_log.info_high("Initialization Step 1b (side_2) Complete.")
        # ---------------------------------------------------------------------
        # Solve unit without heat transfer equation
        self.heat_transfer_equation.deactivate()
        self.side_2.heat.fix(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)))
        self.side_2.heat.unfix()
        self.heat_transfer_equation.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
        self.side_1.release_state(flags1, outlvl=outlvl)
        self.side_2.release_state(flags2, outlvl=outlvl)

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

    def _get_performance_contents(self, time_point=0):
        var_dict = {
            "HX Coefficient":
            self.overall_heat_transfer_coefficient[time_point]
        }
        var_dict["HX Area"] = self.area
        var_dict["Heat Duty"] = self.heat_duty[time_point]
        if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
            var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]}

        expr_dict = {}
        expr_dict["Delta T Driving"] = self.delta_temperature[time_point]
        expr_dict["Delta T In"] = self.delta_temperature_in[time_point]
        expr_dict["Delta T Out"] = self.delta_temperature_out[time_point]

        return {"vars": var_dict, "exprs": expr_dict}

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(
            {
                "Hot Inlet": self.inlet_1,
                "Hot Outlet": self.outlet_1,
                "Cold Inlet": self.inlet_2,
                "Cold Outlet": self.outlet_2,
            },
            time_point=time_point,
        )

    def get_costing(self, module=costing):
        if not hasattr(self.flowsheet(), "costing"):
            self.flowsheet().get_costing()
        self.costing = Block()

        module.hx_costing(self.costing)
コード例 #22
0
class ConcreteTubeSideData(UnitModelBlockData):
    """ConcreteTubeSide 1D 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_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(
        "has_phase_equilibrium",
        ConfigValue(
            default=False,
            domain=In([True, False]),
            description="Phase equilibrium term construction flag",
            doc="""Argument to enable phase equilibrium on the shell side.
- True - include phase equilibrium term
- False - do not include phase equilibrium term""",
        ),
    )
    CONFIG.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""",
        ),
    )
    CONFIG.declare(
        "property_package_args",
        ConfigValue(
            default={},
            description="Arguments for constructing shell 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)""",
        ),
    )
    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 transformating domain. See
Pyomo documentation for supported schemes.""",
        ),
    )

    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(
        "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=3)""",
        ),
    )
    CONFIG.declare(
        "flow_type",
        ConfigValue(
            default=HeatExchangerFlowPattern.cocurrent,
            domain=In(HeatExchangerFlowPattern),
            description="Flow configuration of concrete tube",
            doc="""Flow configuration of concrete tube
- HeatExchangerFlowPattern.cocurrent: shell and tube flows from 0 to 1
(default)
- HeatExchangerFlowPattern.countercurrent: shell side flows from 0 to 1
tube side flows from 1 to 0""",
        ),
    )

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

        Args:
            None

        Returns:
            None
        """

        # Call UnitModel.build to setup dynamics
        super().build()

        # dicretisation if not specified.
        if self.config.flow_type == HeatExchangerFlowPattern.cocurrent:

            set_direction_tube = FlowDirection.forward

            if self.config.transformation_method is useDefault:
                _log.warning("Discretization method was "
                             "not specified for the tube side of the "
                             "co-current concrete tube. "
                             "Defaulting to finite "
                             "difference method on the tube side.")
                self.config.transformation_method = "dae.finite_difference"

            if self.config.transformation_scheme is useDefault:
                _log.warning("Discretization scheme was "
                             "not specified for the tube side of the "
                             "co-current concrete tube. "
                             "Defaulting to backward finite "
                             "difference on the tube side.")
                self.config.transformation_scheme = "BACKWARD"
        elif self.config.flow_type == HeatExchangerFlowPattern.countercurrent:
            set_direction_tube = FlowDirection.backward

            if self.config.transformation_method is useDefault:
                _log.warning("Discretization method was "
                             "not specified for the tube side of the "
                             "counter-current concrete tube. "
                             "Defaulting to finite "
                             "difference method on the tube side.")
                self.config.transformation_method = "dae.finite_difference"

            if self.config.transformation_scheme is useDefault:
                _log.warning("Discretization scheme was "
                             "not specified for the tube side of the "
                             "counter-current concrete tube. "
                             "Defaulting to forward finite "
                             "difference on the tube side.")
                self.config.transformation_scheme = "BACKWARD"
        else:
            raise ConfigurationError(
                "{} ConcreteTubeSide only supports cocurrent and "
                "countercurrent flow patterns, but flow_type configuration"
                " argument was set to {}.".format(self.name,
                                                  self.config.flow_type))

        self.tube = 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,
                "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.tube.add_geometry(flow_direction=set_direction_tube)

        self.tube.add_state_blocks(
            information_flow=set_direction_tube,
            has_phase_equilibrium=self.config.has_phase_equilibrium,
        )

        # Populate tube
        self.tube.add_material_balances(
            balance_type=self.config.material_balance_type,
            has_phase_equilibrium=self.config.has_phase_equilibrium,
        )

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

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

        self.tube.apply_transformation()

        # Add Ports for tube side
        self.add_inlet_port(name="tube_inlet", block=self.tube)
        self.add_outlet_port(name="tube_outlet", block=self.tube)

        # Add reference to control volume geometry
        add_object_reference(self, "tube_area", self.tube.area)
        add_object_reference(self, "tube_length", self.tube.length)

        self._make_performance()

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

        Args:
            None

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

        self.d_tube_outer = Var(
            domain=PositiveReals,
            initialize=0.011,
            doc="Outer diameter of tube",
            units=tube_units("length"),
        )
        self.d_tube_inner = Var(
            domain=PositiveReals,
            initialize=0.010,
            doc="Inner diameter of tube",
            units=tube_units("length"),
        )

        self.tube_heat_transfer_coefficient = Var(
            self.flowsheet().config.time,
            self.tube.length_domain,
            domain=PositiveReals,
            initialize=50,
            doc="Heat transfer coefficient",
            units=tube_units("heat_transfer_coefficient"),
        )

        self.temperature_wall = Var(
            self.flowsheet().config.time,
            self.tube.length_domain,
            domain=PositiveReals,
            initialize=298.15,
            units=tube_units("temperature"),
        )

        # Energy transfer between tube wall and tube
        @self.Constraint(
            self.flowsheet().config.time,
            self.tube.length_domain,
            doc="Convective heat transfer",
        )
        def tube_heat_transfer_eq(self, t, x):
            return self.tube.heat[t, x] == self.tube_heat_transfer_coefficient[
                t, x] * c.pi * pyunits.convert(
                    self.d_tube_inner, to_units=tube_units("length")) * (
                        pyunits.convert(self.temperature_wall[t, x],
                                        to_units=tube_units("temperature")) -
                        self.tube.properties[t, x].temperature)

        # Define tube area in terms of tube diameter
        self.area_calc_tube = Constraint(
            expr=4 * self.tube_area == c.pi * pyunits.convert(
                self.d_tube_inner, to_units=tube_units("length"))**2)

    def initialize(self,
                   state_args=None,
                   outlvl=idaeslog.NOTSET,
                   solver=None,
                   optarg=None):
        """
        Initialization routine for the 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 initialization routine
            optarg : solver options dictionary object (default={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

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

        solver = get_solver(solver=solver, options=optarg)

        flags_tube = self.tube.initialize(
            outlvl=outlvl,
            optarg=optarg,
            solver=solver,
            state_args=state_args,
        )

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

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

        self.tube.release_state(flags_tube)

        init_log.info("Initialization Complete.")

    def _get_performance_contents(self, time_point=0):
        var_dict = {}
        var_dict["Tube Area"] = self.tube.area
        var_dict["Tube Outer Diameter"] = self.d_tube_outer
        var_dict["Tube Inner Diameter"] = self.d_tube_inner
        var_dict["Tube Length"] = self.tube.length

        return {"vars": var_dict}

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(
            {
                "Tube Inlet": self.tube_inlet,
                "Tube Outlet": self.tube_outlet,
            },
            time_point=time_point,
        )

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

        for i, c in self.tube_heat_transfer_eq.items():
            iscale.constraint_scaling_transform(
                c,
                iscale.get_scaling_factor(self.tube.heat[i],
                                          default=1,
                                          warning=True))
コード例 #23
0
class HelmNtuCondenserData(UnitModelBlockData):
    """
    Simple NTU condenser unit model.  This model assumes the property pacakages
    specified are Helmholtz EOS type.
    """
    CONFIG = UnitModelBlockData.CONFIG(implicit=True)
    _make_heat_exchanger_config(CONFIG)

    def _process_config(self):
        """Check for configuration errors and alternate config option names.
        """
        config = self.config

        if config.hot_side_name == config.cold_side_name:
            raise NameError(
                "Condenser hot and cold side cannot have the same name '{}'."
                " Be sure to set both the hot_side_name and cold_side_name.".
                format(config.hot_side_name))
        for o in config:
            if not (o in self.CONFIG
                    or o in [config.hot_side_name, config.cold_side_name]):
                raise KeyError(
                    "Condenser config option {} not defined".format(o))

        if config.hot_side_name in config:
            config.hot_side_config.set_value(config[config.hot_side_name])
            # Allow access to hot_side_config under the hot_side_name
            setattr(config, config.hot_side_name, config.hot_side_config)
        if config.cold_side_name in config:
            config.cold_side_config.set_value(config[config.cold_side_name])
            # Allow access to hot_side_config under the cold_side_name
            setattr(config, config.cold_side_name, config.cold_side_config)

        if config.cold_side_name in ["hot_side", "side_1"]:
            raise ConfigurationError(
                "Cold side name cannot be in ['hot_side', 'side_1'].")
        if config.hot_side_name in ["cold_side", "side_2"]:
            raise ConfigurationError(
                "Hot side name cannot be in ['cold_side', 'side_2'].")

    def build(self):
        """
        Building model

        Args:
            None
        Returns:
            None
        """
        ########################################################################
        #  Call UnitModel.build to setup dynamics and configure                #
        ########################################################################
        super().build()
        self._process_config()
        config = self.config
        time = self.flowsheet().config.time

        ########################################################################
        # Add control volumes                                                  #
        ########################################################################
        hot_side = _make_heater_control_volume(
            self,
            config.hot_side_name,
            config.hot_side_config,
            dynamic=config.dynamic,
            has_holdup=config.has_holdup,
        )
        cold_side = _make_heater_control_volume(
            self,
            config.cold_side_name,
            config.cold_side_config,
            dynamic=config.dynamic,
            has_holdup=config.has_holdup,
        )
        # Add refernces to the hot side and cold side, so that we have solid
        # names to refere to internally.  side_1 and side_2 also maintain
        # compatability with older models.  Using add_object_reference keeps
        # these from showing up when you iterate through pyomo compoents in a
        # model, so only the user specified control volume names are "seen"
        if not hasattr(self, "side_1"):
            add_object_reference(self, "side_1", hot_side)
        if not hasattr(self, "side_2"):
            add_object_reference(self, "side_2", cold_side)
        if not hasattr(self, "hot_side"):
            add_object_reference(self, "hot_side", hot_side)
        if not hasattr(self, "cold_side"):
            add_object_reference(self, "cold_side", cold_side)

        ########################################################################
        # Add variables                                                        #
        ########################################################################
        # Use hot side units as basis
        s1_metadata = config.hot_side_config.property_package.get_metadata()

        f_units = s1_metadata.get_derived_units("flow_mole")
        cp_units = s1_metadata.get_derived_units("heat_capacity_mole")
        q_units = s1_metadata.get_derived_units("power")
        u_units = s1_metadata.get_derived_units("heat_transfer_coefficient")
        a_units = s1_metadata.get_derived_units("area")
        temp_units = s1_metadata.get_derived_units("temperature")

        self.overall_heat_transfer_coefficient = pyo.Var(
            time,
            domain=pyo.PositiveReals,
            initialize=100.0,
            doc="Overall heat transfer coefficient",
            units=u_units,
        )
        self.area = pyo.Var(
            domain=pyo.PositiveReals,
            initialize=1000.0,
            doc="Heat exchange area",
            units=a_units,
        )
        self.heat_duty = pyo.Reference(cold_side.heat)
        ########################################################################
        # Add ports                                                            #
        ########################################################################
        i1 = self.add_inlet_port(name=f"{config.hot_side_name}_inlet",
                                 block=hot_side,
                                 doc="Hot side inlet")
        i2 = self.add_inlet_port(name=f"{config.cold_side_name}_inlet",
                                 block=cold_side,
                                 doc="Cold side inlet")
        o1 = self.add_outlet_port(name=f"{config.hot_side_name}_outlet",
                                  block=hot_side,
                                  doc="Hot side outlet")
        o2 = self.add_outlet_port(name=f"{config.cold_side_name}_outlet",
                                  block=cold_side,
                                  doc="Cold side outlet")

        # Using Andrew's function for now.  I want these port names for backward
        # compatablity, but I don't want them to appear if you iterate throught
        # components and add_object_reference hides them from Pyomo.
        if not hasattr(self, "inlet_1"):
            add_object_reference(self, "inlet_1", i1)
        if not hasattr(self, "inlet_2"):
            add_object_reference(self, "inlet_2", i2)
        if not hasattr(self, "outlet_1"):
            add_object_reference(self, "outlet_1", o1)
        if not hasattr(self, "outlet_2"):
            add_object_reference(self, "outlet_2", o2)

        if not hasattr(self, "hot_inlet"):
            add_object_reference(self, "hot_inlet", i1)
        if not hasattr(self, "cold_inlet"):
            add_object_reference(self, "cold_inlet", i2)
        if not hasattr(self, "hot_outlet"):
            add_object_reference(self, "hot_outlet", o1)
        if not hasattr(self, "cold_outlet"):
            add_object_reference(self, "cold_outlet", o2)

        ########################################################################
        # Add a unit level energy balance                                      #
        ########################################################################
        @self.Constraint(time, doc="Heat balance equation")
        def unit_heat_balance(b, t):
            return 0 == (hot_side.heat[t] +
                         pyunits.convert(cold_side.heat[t], to_units=q_units))

        ########################################################################
        # Add some useful expressions for condenser performance                #
        ########################################################################

        @self.Expression(time, doc="Inlet temperature difference")
        def delta_temperature_in(b, t):
            return (hot_side.properties_in[t].temperature - pyunits.convert(
                cold_side.properties_in[t].temperature, temp_units))

        @self.Expression(time, doc="Outlet temperature difference")
        def delta_temperature_out(b, t):
            return (hot_side.properties_out[t].temperature - pyunits.convert(
                cold_side.properties_out[t].temperature, temp_units))

        @self.Expression(time, doc="NTU Based temperature difference")
        def delta_temperature_ntu(b, t):
            return (hot_side.properties_in[t].temperature_sat -
                    pyunits.convert(cold_side.properties_in[t].temperature,
                                    temp_units))

        @self.Expression(
            time,
            doc="Minimum product of flow rate and heat "
            "capacity (always on tube side since shell side has phase change)")
        def mcp_min(b, t):
            return pyunits.convert(
                cold_side.properties_in[t].flow_mol *
                cold_side.properties_in[t].cp_mol_phase['Liq'],
                f_units * cp_units)

        @self.Expression(time, doc="Number of transfer units (NTU)")
        def ntu(b, t):
            return b.overall_heat_transfer_coefficient[t] * b.area / b.mcp_min[
                t]

        @self.Expression(time, doc="Condenser effectiveness factor")
        def effectiveness(b, t):
            return 1 - pyo.exp(-self.ntu[t])

        @self.Expression(time, doc="Heat treansfer")
        def heat_transfer(b, t):
            return b.effectiveness[t] * b.mcp_min[t] * b.delta_temperature_ntu[
                t]

        ########################################################################
        # Add Equations to calculate heat duty based on NTU method             #
        ########################################################################
        @self.Constraint(time,
                         doc="Heat transfer rate equation based on NTU method")
        def heat_transfer_equation(b, t):
            return (pyunits.convert(cold_side.heat[t],
                                    q_units) == self.heat_transfer[t])

        @self.Constraint(
            time, doc="Shell side outlet enthalpy is saturated water enthalpy")
        def saturation_eqn(b, t):
            return (hot_side.properties_out[t].enth_mol ==
                    hot_side.properties_in[t].enth_mol_sat_phase["Liq"])

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

    def initialize(
        self,
        state_args_1=None,
        state_args_2=None,
        unfix='hot_flow',
        outlvl=idaeslog.NOTSET,
        solver=None,
        optarg=None,
    ):
        """
        Condenser initialization method. The initialization routine assumes
        fixed area and heat transfer coefficient and adjusts the cooling water
        flow to condense steam to saturated water at shell side pressure.

        Args:
            state_args_1 : a dict of arguments to be passed to the property
                initialization for hot side (see documentation of the specific
                property package) (default = None).
            state_args_2 : a dict of arguments to be passed to the property
                initialization for cold side (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 default solver)

        Returns:
            None

        """
        if unfix not in {"hot_flow", "cold_flow", "pressure"}:
            raise Exception("Condenser free variable must be in 'hot_flow', "
                            "'cold_flow', or 'pressure'")
        # Set solver options
        init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit")
        solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit")

        hot_side = getattr(self, self.config.hot_side_name)
        cold_side = getattr(self, self.config.cold_side_name)

        # Store initial model specs, restored at the end of initializtion, so
        # the problem is not altered.  This can restore fixed/free vars,
        # active/inactive constraints, and fixed variable values.
        sp = StoreSpec.value_isfixed_isactive(only_fixed=True)
        istate = to_json(self, return_dict=True, wts=sp)

        # Create solver
        opt = get_solver(solver, optarg)

        flags1 = hot_side.initialize(outlvl=outlvl,
                                     optarg=optarg,
                                     solver=solver,
                                     state_args=state_args_1)
        flags2 = cold_side.initialize(outlvl=outlvl,
                                      optarg=optarg,
                                      solver=solver,
                                      state_args=state_args_2)
        init_log.info_high("Initialization Step 1 Complete.")

        # Solve with all constraints activated
        self.saturation_eqn.activate()
        if unfix == 'pressure':
            hot_side.properties_in[:].pressure.unfix()
        elif unfix == 'hot_flow':
            hot_side.properties_in[:].flow_mol.unfix()
        elif unfix == 'cold_flow':
            cold_side.properties_in[:].flow_mol.unfix()

        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)
        cold_side.release_state(flags2, outlvl)
        from_json(self, sd=istate, wts=sp)

    def _get_performance_contents(self, time_point=0):
        var_dict = {
            "HX Coefficient":
            self.overall_heat_transfer_coefficient[time_point]
        }
        var_dict["HX Area"] = self.area
        var_dict["Heat Duty"] = self.heat_duty[time_point]
        expr_dict = {}
        expr_dict["Delta T In"] = self.delta_temperature_in[time_point]
        expr_dict["Delta T Out"] = self.delta_temperature_out[time_point]

        return {"vars": var_dict, "exprs": expr_dict}

    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):
        if not hasattr(self.flowsheet(), "costing"):
            self.flowsheet().get_costing()
        self.costing = pyo.Block()

        module.hx_costing(self.costing)

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

        area_sf_default = 1e-2
        overall_heat_transfer_coefficient_sf_default = 1e-2

        # Function to set defaults so I don't need to reproduce the same code
        def _fill_miss_with_default(name, s):
            try:
                c = getattr(self, name)
            except AttributeError:
                return  # it's okay if the attribute doesn't exist, spell careful
            if iscale.get_scaling_factor(c) is None:
                for ci in c.values():
                    if iscale.get_scaling_factor(ci) is None:
                        iscale.set_scaling_factor(ci, s)

        # Set defaults where scale factors are missing
        _fill_miss_with_default("area", area_sf_default)
        _fill_miss_with_default("overall_heat_transfer_coefficient",
                                overall_heat_transfer_coefficient_sf_default)

        for t, c in self.heat_transfer_equation.items():
            sf = iscale.get_scaling_factor(self.cold_side.heat[t])
            iscale.constraint_scaling_transform(c, sf, overwrite=False)

        for t, c in self.unit_heat_balance.items():
            sf = iscale.get_scaling_factor(self.cold_side.heat[t])
            iscale.constraint_scaling_transform(c, sf, overwrite=False)

        for t, c in self.saturation_eqn.items():
            sf = iscale.get_scaling_factor(
                self.hot_side.properties_out[t].enth_mol)
            iscale.constraint_scaling_transform(c, sf, overwrite=False)
コード例 #24
0
ファイル: pid_controller.py プロジェクト: jmorgan29/idaes-pse
class PIDControllerData(UnitModelBlockData):
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare(
        "pv",
        ConfigValue(
            default=None,
            description="Process variable to be controlled",
            doc="A Pyomo Var, Expression, or Reference for the measured"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "mv",
        ConfigValue(
            default=None,
            description="Manipulated process variable",
            doc="A Pyomo Var, Expression, or Reference for the controlled"
            " process variable. Should be indexed by time."))
    CONFIG.declare(
        "bounded_output",
        ConfigValue(
            default=False,
            description="Flag to bound manipulated variable",
            doc=
            """Indicating if the output for the manipulated variable is bounded
Default: False.
If True, user need to set the lower and upper bound parameters"""))
    CONFIG.declare(
        "type",
        ConfigValue(default="PI",
                    domain=In(['P', 'PI', 'PD', 'PID']),
                    description="Control type",
                    doc="""Controller type options including
- P: Proportional only
- PI: Proportional and integral only
- PD: Proportional and derivative only
- PID: Proportional, integral and derivative
Default is PI"""))

    def __init__(self, *args, **kwargs):
        deprecation_warning(
            "DEPRECATED: The idaes.power_generation.control.pid_controller.PIDController"
            " model is deprecated and will be removed. Use"
            " idaes.generic_models.control.PIDController instead.",
            version=1.12,
        )
        super().__init__(*args, **kwargs)

    def build(self):
        """
        Build the PID block
        """
        super().build()

        # Do nothing if steady-state
        if self.config.dynamic is True:
            # Check for required config
            if self.config.pv is None:
                raise ConfigurationError("Controller configuration"
                                         " requires 'pv'")
            if self.config.mv is None:
                raise ConfigurationError("Controller configuration"
                                         " requires 'mv'")
            # Shorter pointers to time set information
            time_set = self.flowsheet().time
            self.pv = Reference(self.config.pv)
            self.mv = Reference(self.config.mv)

            # Parameters
            self.mv_lb = Param(mutable=True,
                               initialize=0.05,
                               doc="Controller output lower bound")
            self.mv_ub = Param(mutable=True,
                               initialize=1,
                               doc="Controller output upper bound")

            # Variable for basic controller settings may change with time.
            self.setpoint = Var(time_set, initialize=0.5, doc="Setpoint")
            self.gain_p = Var(time_set,
                              initialize=0.1,
                              doc="Gain for proportional part")
            if self.config.type == 'PI' or self.config.type == 'PID':
                self.gain_i = Var(time_set,
                                  initialize=0.1,
                                  doc="Gain for integral part")
            if self.config.type == 'PD' or self.config.type == 'PID':
                self.gain_d = Var(time_set,
                                  initialize=0.01,
                                  doc="Gain for derivative part")
            self.mv_ref = Var(initialize=0.5,
                              doc="bias value of manipulated variable")

            if self.config.type == 'P' or self.config.type == 'PI':

                @self.Expression(time_set, doc="Error expression")
                def error(b, t):
                    return b.setpoint[t] - b.pv[t]
            else:
                self.error = Var(time_set, initialize=0, doc="Error variable")

                @self.Constraint(time_set, doc="Error variable")
                def error_eqn(b, t):
                    return b.error[t] == b.setpoint[t] - b.pv[t]

            if self.config.type == 'PI' or self.config.type == 'PID':
                self.integral_of_error = Var(time_set,
                                             initialize=0,
                                             doc="Integral term")
                self.error_from_integral = DerivativeVar(
                    self.integral_of_error,
                    wrt=self.flowsheet().time,
                    initialize=0)

                @self.Constraint(time_set,
                                 doc="Error calculated by"
                                 " derivative of integral")
                def error_from_integral_eqn(b, t):
                    return b.error[t] == b.error_from_integral[t]

            if self.config.type == 'PID' or self.config.type == 'PD':
                self.derivative_of_error = DerivativeVar(
                    self.error, wrt=self.flowsheet().time, initialize=0)

            @self.Expression(time_set, doc="Proportional output")
            def mv_p_only(b, t):
                return b.gain_p[t] * b.error[t]

            @self.Expression(time_set, doc="Proportional output and reference")
            def mv_p_only_with_ref(b, t):
                return b.gain_p[t] * b.error[t] + b.mv_ref

            if self.config.type == 'PI' or self.config.type == 'PID':

                @self.Expression(time_set, doc="Integral output")
                def mv_i_only(b, t):
                    return b.gain_i[t] * b.integral_of_error[t]

            if self.config.type == 'PD' or self.config.type == 'PID':

                @self.Expression(time_set, doc="Derivative output")
                def mv_d_only(b, t):
                    return b.gain_d[t] * b.derivative_of_error[t]

            @self.Expression(time_set,
                             doc="Unbounded output for manimulated variable")
            def mv_unbounded(b, t):
                if self.config.type == 'PID':
                    return (b.mv_ref + b.gain_p[t] * b.error[t] +
                            b.gain_i[t] * b.integral_of_error[t] +
                            b.gain_d[t] * b.derivative_of_error[t])
                elif self.config.type == 'PI':
                    return (b.mv_ref + b.gain_p[t] * b.error[t] +
                            b.gain_i[t] * b.integral_of_error[t])
                elif self.config.type == 'PD':
                    return (b.mv_ref + b.gain_p[t] * b.error[t] +
                            b.gain_d[t] * b.derivative_of_error[t])
                else:
                    return b.mv_ref + b.gain_p[t] * b.error[t]

            @self.Constraint(time_set,
                             doc="Bounded output of manipulated variable")
            def mv_eqn(b, t):
                if t == b.flowsheet().time.first():
                    return Constraint.Skip
                else:
                    if self.config.bounded_output is True:
                        return (b.mv[t]-b.mv_lb) * \
                            (1+exp(-4/(b.mv_ub-b.mv_lb)
                                   * (b.mv_unbounded[t]
                                      - (b.mv_lb
                                         + b.mv_ub)/2))) == b.mv_ub-b.mv_lb
                    else:
                        return b.mv[t] == b.mv_unbounded[t]

            if self.config.bounded_output is True:
                if self.config.type == 'PI' or self.config.type == 'PID':

                    @self.Expression(time_set,
                                     doc="Integral error"
                                     " at error 0 and mv_ref")
                    def integral_of_error_ref(b, t):
                        return ((b.mv_lb + b.mv_ub) / 2 - b.mv_ref -
                                log((b.mv_ub - b.mv_lb) /
                                    (b.mv_ref - b.mv_lb) - 1) / 4 *
                                (b.mv_ub - b.mv_lb)) / b.gain_i[t]

                    @self.Expression(time_set,
                                     doc="Integral error at error 0 and"
                                     " output value at current mv")
                    def integral_of_error_mv(b, t):
                        return ((b.mv_lb + b.mv_ub) / 2 - b.mv_ref -
                                log((b.mv_ub - b.mv_lb) /
                                    (b.mv[t] - b.mv_lb) - 1) / 4 *
                                (b.mv_ub - b.mv_lb)) / b.gain_i[t]
コード例 #25
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}
コード例 #26
0
ファイル: reboiler.py プロジェクト: bhattade/idaes-pse
class ReboilerData(UnitModelBlockData):
    """
    Reboiler unit for distillation model.
    Unit model to reboil the liquid from the bottom tray of
    the distillation column.
    """
    CONFIG = UnitModelBlockData.CONFIG()
    CONFIG.declare("has_boilup_ratio", ConfigValue(
        default=False,
        domain=In([True, False]),
        description="Boilup ratio term construction flag",
        doc="""Indicates whether terms for boilup ratio should be
constructed,
**default** - False.
**Valid values:** {
**True** - include construction of boilup ratio constraint,
**False** - exclude construction of boilup ratio constraint}"""))
    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
        """
        # Call UnitModel.build to setup dynamics
        super(ReboilerData, self).build()

        # Add Control Volume for the Reboiler
        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)

        if self.config.has_boilup_ratio is True:

            self.boilup_ratio = Var(initialize=0.5,
                                    doc="boilup ratio for reboiler")

            def rule_boilup_ratio(self, t):
                if hasattr(self.control_volume.properties_out[t],
                           "flow_mol_phase"):
                    return self.boilup_ratio * \
                        self.control_volume.properties_out[t].\
                        flow_mol_phase["Liq"] == self.control_volume.\
                        properties_out[t].flow_mol_phase["Vap"]
                elif hasattr(self.control_volume.properties_out[t],
                             "flow_mol_phase_comp"):
                    return self.boilup_ratio * \
                        sum(self.control_volume.properties_out[t].
                            flow_mol_phase_comp["Liq", i]
                            for i in self.control_volume.properties_out[t].
                            _params.component_list) == \
                        sum(self.control_volume.properties_out[t].
                            flow_mol_phase_comp["Vap", i]
                            for i in self.control_volume.properties_out[t].
                            _params.component_list)
                else:
                    raise PropertyNotSupportedError(
                        "Unrecognized names for flow variables encountered "
                        "while building the constraint for reboiler.")
            self.eq_boilup_ratio = Constraint(self.flowsheet().time,
                                              rule=rule_boilup_ratio)

        self._make_ports()

        self._make_splits_reboiler()

        # Add object reference to variables of the control volume
        # Reference to the heat duty
        add_object_reference(self, "heat_duty", self.control_volume.heat)
        # Reference to the pressure drop (if set to True)
        if self.config.has_pressure_change:
            add_object_reference(self, "deltaP", self.control_volume.deltaP)

    def _make_ports(self):

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

        # Outlet ports that always exist irrespective of reboiler type
        self.bottoms = Port(noruleinit=True, doc="Bottoms stream.")

        self.vapor_reboil = Port(noruleinit=True,
                                 doc="Vapor outlet stream that is returned to "
                                 "to the bottom tray.")

    def _make_splits_reboiler(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.bottoms.add(Reference(var), k)

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

            elif "frac" in k and ("mole" in k or "mass" 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:
                    # Assuming the state block has the var
                    # "mole_frac_phase_comp". Valid if VLE is supported
                    # Create a string "mole_frac_phase_comp" or
                    # "mass_frac_phase_comp". Cannot directly append phase to
                    # k as the naming convention is phase followed by comp

                    str_split = k.split('_')
                    local_name = '_'.join(str_split[0:2]) + \
                        "_phase" + "_" + str_split[2]

                    # Rule for liquid fraction
                    def rule_liq_frac(self, t, i):
                        return self.control_volume.properties_out[t].\
                            component(local_name)["Liq", i]
                    self.e_liq_frac = Expression(
                        self.flowsheet().time, index_set,
                        rule=rule_liq_frac)

                    # Rule for vapor fraction
                    def rule_vap_frac(self, t, i):
                        return self.control_volume.properties_out[t].\
                            component(local_name)["Vap", i]
                    self.e_vap_frac = Expression(
                        self.flowsheet().time, index_set,
                        rule=rule_vap_frac)

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

                    # add the reference and variable name to the
                    # vapor port
                    self.vapor_reboil.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 distillate port
                    self.bottoms.add(Reference(var), k)

                    # add the reference and variable name to the boil up port
                    self.vapor_reboil.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 flow
                        def rule_vap_flow(self, t):
                            return self.control_volume.properties_out[t].\
                                component(local_name)["Vap"]
                        self.e_vap_flow = Expression(
                            self.flowsheet().time,
                            rule=rule_vap_flow)

                        # Rule to link the liq flow to the distillate
                        def rule_bottoms_flow(self, t):
                            return self.control_volume.properties_out[t].\
                                component(local_name)["Liq"]
                        self.e_bottoms_flow = Expression(
                            self.flowsheet().time,
                            rule=rule_bottoms_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 flow
                        def rule_vap_flow(self, t, i):
                            return self.control_volume.properties_out[t].\
                                component(local_name)["Vap", i]
                        self.e_vap_flow = Expression(
                            self.flowsheet().time, index_set,
                            rule=rule_vap_flow)

                        # Rule to link the liq flow to the distillate
                        def rule_bottoms_flow(self, t, i):
                            return self.control_volume.properties_out[t].\
                                component(local_name)["Liq", i]
                        self.e_bottoms_flow = Expression(
                            self.flowsheet().time, index_set,
                            rule=rule_bottoms_flow)

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

                    # add the reference and variable name to the
                    # distillate port
                    self.vapor_reboil.add(self.e_vap_flow, k)
            elif "enth" in k:
                if "phase" not in k:
                    # 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 = 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.")

                    # 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 self.control_volume.properties_out[t].\
                            component(local_name)["Vap"]
                    self.e_vap_enth = Expression(
                        self.flowsheet().time,
                        rule=rule_vap_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_bottoms_enth(self, t):
                        return self.control_volume.properties_out[t].\
                            component(local_name)["Liq"]
                    self.e_bottoms_enth = Expression(
                        self.flowsheet().time,
                        rule=rule_bottoms_enth)

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

                    # add the reference and variable name to the
                    # distillate port
                    self.vapor_reboil.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 distillate port
                    self.bottoms.add(Reference(var), k)

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

    def initialize(self, solver=None, outlvl=0):

        # TODO: Fix the inlets to the reboiler 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")

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

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

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

        stream_dict = {"Inlet": "inlet",
                       "Vapor Reboil": "vapor_reboil",
                       "Bottoms": "bottoms"}

        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")
コード例 #27
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))
コード例 #28
0
class CSTRData(UnitModelBlockData):
    """
    Standard CSTR Unit Model Class
    """
    CONFIG = UnitModelBlockData.CONFIG()

    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 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(
        "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(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "reaction_package",
        ConfigValue(
            default=None,
            domain=is_reaction_parameter_block,
            description="Reaction package to use for control volume",
            doc=
            """Reaction parameter object used to define reaction calculations,
**default** - None.
**Valid values:** {
**None** - no reaction package,
**ReactionParameterBlock** - a ReactionParameterBlock object.}"""))
    CONFIG.declare(
        "reaction_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing reaction packages",
            doc=
            """A ConfigBlock with arguments to be passed to a reaction block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see reaction package for documentation.}"""))

    def build(self):
        """
        Begin building model (pre-DAE transformation).
        Args:
            None
        Returns:
            None
        """
        # Call UnitModel.build to setup dynamics
        super(CSTRData, self).build()

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

        self.control_volume.add_geometry()

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

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

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

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

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

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

        # Add object references
        add_object_reference(self, "volume", self.control_volume.volume)

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

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

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

    def _get_performance_contents(self, time_point=0):
        var_dict = {"Volume": self.volume[time_point]}
        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}
コード例 #29
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 shell and tube.
        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.shell.scaling_factor_energy.value = f
        self.tube.scaling_factor_energy.value = f

    def build(self):
        """
        Building model

        Args:
            None
        Returns:
            None
        """
        ########################################################################
        #  Call UnitModel.build to setup dynamics and configure                #
        ########################################################################
        super().build()
        config = self.config
        ########################################################################
        # Add variables                                                        #
        ########################################################################
        u = self.overall_heat_transfer_coefficient = Var(
            self.flowsheet().config.time,
            domain=PositiveReals,
            initialize=100.0,
            doc="Overall heat transfer coefficient")
        a = self.area = Var(domain=PositiveReals,
                            initialize=1000.0,
                            doc="Heat exchange area")
        self.delta_temperature_in = Var(
            self.flowsheet().config.time,
            initialize=10.0,
            doc="Temperature difference at the shell inlet end")
        self.delta_temperature_out = Var(
            self.flowsheet().config.time,
            initialize=10.0,
            doc="Temperature difference at the shell outlet end")
        if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
            self.crossflow_factor = Var(
                self.flowsheet().config.time,
                initialize=1.0,
                doc="Factor to adjust coutercurrent flow heat "
                "transfer calculation for cross flow.")
            f = self.crossflow_factor
        ########################################################################
        # Add control volumes                                                  #
        ########################################################################
        _make_heater_control_volume(self,
                                    "shell",
                                    config.shell,
                                    dynamic=config.dynamic,
                                    has_holdup=config.has_holdup)
        _make_heater_control_volume(self,
                                    "tube",
                                    config.tube,
                                    dynamic=config.dynamic,
                                    has_holdup=config.has_holdup)
        # Add convienient references to heat duty.
        q = self.heat_duty = Reference(self.tube.heat)
        ########################################################################
        # Add ports                                                            #
        ########################################################################
        self.add_inlet_port(name="inlet_1", block=self.shell)
        self.add_inlet_port(name="inlet_2", block=self.tube)
        self.add_outlet_port(name="outlet_1", block=self.shell)
        self.add_outlet_port(name="outlet_2", block=self.tube)
        ########################################################################
        # Add end temperaure differnece constraints                            #
        ########################################################################
        @self.Constraint(self.flowsheet().config.time)
        def delta_temperature_in_equation(b, t):
            if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent:
                return (b.delta_temperature_in[t] ==
                        b.shell.properties_in[t].temperature -
                        b.tube.properties_in[t].temperature)
            else:
                return (b.delta_temperature_in[t] ==
                        b.shell.properties_in[t].temperature -
                        b.tube.properties_out[t].temperature)

        @self.Constraint(self.flowsheet().config.time)
        def delta_temperature_out_equation(b, t):
            if b.config.flow_pattern == HeatExchangerFlowPattern.cocurrent:
                return (b.delta_temperature_out[t] ==
                        b.shell.properties_out[t].temperature -
                        b.tube.properties_out[t].temperature)
            else:
                return (b.delta_temperature_out[t] ==
                        b.shell.properties_out[t].temperature -
                        b.tube.properties_in[t].temperature)

        ########################################################################
        # Add a unit level energy balance                                      #
        ########################################################################
        @self.Constraint(self.flowsheet().config.time)
        def unit_heat_balance(b, t):
            return 0 == self.shell.heat[t] + self.tube.heat[t]

        ########################################################################
        # Add delta T calculations using callack function, lots of options,    #
        #   and users can provide their own if needed                          #
        ########################################################################
        config.delta_temperature_callback(self)
        ########################################################################
        # Add Heat transfer equation                                           #
        ########################################################################
        deltaT = self.delta_temperature
        scale = self.shell.scaling_factor_energy

        @self.Constraint(self.flowsheet().config.time)
        def heat_transfer_equation(b, t):
            if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
                return 0 == (f[t] * u[t] * a * deltaT[t] - q[t]) * scale
            else:
                return 0 == (u[t] * a * deltaT[t] - q[t]) * scale

        ########################################################################
        # Add symbols for LaTeX equation rendering                             #
        ########################################################################
        self.overall_heat_transfer_coefficient.latex_symbol = "U"
        self.area.latex_symbol = "A"
        self.shell.heat.latex_symbol = "Q_1"
        self.tube.heat.latex_symbol = "Q_2"
        self.delta_temperature.latex_symbol = "\\Delta T"

    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 shell (see documentation of the specific
                property package) (default = {}).
            state_args_2 : a dict of arguments to be passed to the property
                initialization for tube (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.shell.initialize(outlvl=outlvl - 1,
                                       optarg=optarg,
                                       solver=solver,
                                       state_args=state_args_1)

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

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

        if outlvl > 0:
            _log.info('{} Initialization Step 1b (tube) Complete.'.format(
                self.name))
        # ---------------------------------------------------------------------
        # Solve unit without heat transfer equation
        self.heat_transfer_equation.deactivate()
        self.tube.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.tube.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.shell.release_state(flags1, outlvl - 1)
        self.tube.release_state(flags2, outlvl - 1)

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

    def _get_performance_contents(self, time_point=0):
        var_dict = {
            "HX Coefficient":
            self.overall_heat_transfer_coefficient[time_point]
        }
        var_dict["HX Area"] = self.area
        var_dict["Heat Duty"] = self.heat_duty[time_point]
        if self.config.flow_pattern == HeatExchangerFlowPattern.crossflow:
            var_dict = {"Crossflow Factor": self.crossflow_factor[time_point]}

        expr_dict = {}
        expr_dict["Delta T Driving"] = self.delta_temperature[time_point]
        expr_dict["Delta T In"] = self.delta_temperature_in[time_point]
        expr_dict["Delta T Out"] = self.delta_temperature_out[time_point]

        return {"vars": var_dict, "exprs": expr_dict}

    def _get_stream_table_contents(self, time_point=0):
        return create_stream_table_dataframe(
            {
                "Shell Inlet": self.inlet_1,
                "Shell Outlet": self.outlet_1,
                "Tube Inlet": self.inlet_2,
                "Tube Outlet": self.outlet_2
            },
            time_point=time_point)

    def get_costing(self, module=costing):
        if not hasattr(self.flowsheet(), 'costing'):
            self.flowsheet().get_costing()
        self.costing = Block()

        module.hx_costing(self.costing)
コード例 #30
0
class WaterPipeData(UnitModelBlockData):
    """
    Water or steam pipe Unit Class
    """
    CONFIG = UnitModelBlockData.CONFIG()
    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=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=True,
            domain=In([True, False]),
            description="Pressure change term construction flag",
            doc="""Indicates whether terms for pressure change should be
constructed,
**default** - False.
**Valid values:** {
**True** - include pressure change terms,
**False** - exclude pressure change terms.}"""))
    CONFIG.declare(
        "property_package",
        ConfigValue(
            default=useDefault,
            domain=is_physical_parameter_block,
            description="Property package to use for control volume",
            doc=
            """Property parameter object used to define property calculations,
**default** - useDefault.
**Valid values:** {
**useDefault** - use default package from parent model or flowsheet,
**PhysicalParameterObject** - a PhysicalParameterBlock object.}"""))
    CONFIG.declare(
        "property_package_args",
        ConfigBlock(
            implicit=True,
            description="Arguments to use for constructing property packages",
            doc=
            """A ConfigBlock with arguments to be passed to a property block(s)
and used when constructing these,
**default** - None.
**Valid values:** {
see property package for documentation.}"""))
    CONFIG.declare(
        "contraction_expansion_at_end",
        ConfigValue(default='None',
                    domain=In(['None', 'contraction', 'expansion']),
                    description='Any contraction or expansion at the end',
                    doc='Define if pressure drop due to contraction'
                    ' or expansion needs to be considered'))
    CONFIG.declare(
        "water_phase",
        ConfigValue(default='Liq',
                    domain=In(['Liq', 'Vap']),
                    description='Water phase',
                    doc='''Define water phase for property calls,
mixed phase not supported'''))

    def build(self):
        """
        Begin building model
        """
        # Call UnitModel.build to setup dynamics
        super(WaterPipeData, self).build()

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

        self.control_volume.add_geometry()

        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)

        if (self.config.has_pressure_change is True
                and self.config.momentum_balance_type != 'none'):
            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
        """
        # Number of pipe
        self.number_of_pipes = Var(initialize=4, doc="Number of pipes")
        # Length of pipe
        self.length = Var(initialize=10.0,
                          doc="Total length of the straight pipe")
        # Elevation change of pipe, elevation of outlet - elevation of inlet
        self.elevation_change = Var(
            initialize=10.0, doc="Change of elevation from inlet to outlet")
        # Inside diameter of pipe
        self.diameter = Var(initialize=0.6, doc="Inside diameter of pipe")

        if not self.config.contraction_expansion_at_end == "None":
            self.area_ratio = Var(
                initialize=1,
                doc="Cross section area ratio of exit end to pipe")

        # Volume constraint
        @self.Constraint(self.flowsheet().config.time,
                         doc="Total volume of all pipes")
        def volume_eqn(b, t):
            return b.volume[t] == 0.25 * const.pi * b.diameter**2 \
                * b.length * b.number_of_pipes

    def _make_performance(self):
        """
        Define constraints which describe the behaviour of the unit model.
        """
        phase = self.config.water_phase

        # Add performance variables
        # Velocity of fluid inside pipe
        self.velocity = Var(self.flowsheet().config.time,
                            initialize=10.0,
                            doc='Fluid velocity inside pipe')

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

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

        # Correction factor for pressure drop due to friction
        self.fcorrection_dp = Var(initialize=1.0,
                                  doc="Correction factor for pressure drop")

        # Pressure change due to friction
        self.deltaP_friction = Var(self.flowsheet().config.time,
                                   initialize=-1.0,
                                   doc='Pressure change due to friction')

        # Pressure change due to gravity
        self.deltaP_gravity = Var(self.flowsheet().config.time,
                                  initialize=0.0,
                                  doc='Pressure change due to gravity')

        # Pressure change due to area change at end
        self.deltaP_area_change = Var(self.flowsheet().config.time,
                                      initialize=0.0,
                                      doc='Pressure change due to area change')

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

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

        # Friction factor expression depending on laminar or turbulent flow
        @self.Constraint(self.flowsheet().config.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 * b.fcorrection_dp

        # Pressure change equation for friction
        @self.Constraint(self.flowsheet().config.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[phase] * \
                   b.velocity[t]**2 * b.friction_factor_darcy[t] * b.length

        # Pressure change equation for gravity
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pressure change due to gravity")
        def pressure_change_gravity_eqn(b, t):
            return b.deltaP_gravity[t] == -const.acceleration_gravity * \
                b.control_volume.properties_in[t].dens_mass_phase[phase]\
                * b.elevation_change

        # Pressure change equation for contraction or expansion
        @self.Constraint(self.flowsheet().config.time,
                         doc="Pressure change due to gravity")
        def pressure_change_area_change_eqn(b, t):
            if self.config.contraction_expansion_at_end == "contraction":
                return b.deltaP_area_change[t] == - (0.1602*b.area_ratio**2
                                                     - 0.646*b.area_ratio
                                                     + 1.4858) * \
                    0.5 * b.control_volume.properties_out[t].\
                    dens_mass_phase[phase] * (b.velocity[t]/b.area_ratio)**2 \
                    + 0.5*b.control_volume.properties_out[t].\
                    dens_mass_phase[phase]*b.velocity[t]**2
            elif self.config.contraction_expansion_at_end == "expansion":
                return b.deltaP_area_change[t] == \
                    b.control_volume.properties_out[t].dens_mass_phase[phase]\
                    * b.velocity[t]**2*(b.area_ratio-1)/b.area_ratio**2
            else:
                return b.deltaP_area_change[t] == 0

        # Total pressure change equation
        @self.Constraint(self.flowsheet().config.time, doc="Pressure drop")
        def pressure_change_total_eqn(b, t):
            return b.deltaP[t] == (b.deltaP_friction[t] + b.deltaP_gravity[t] +
                                   b.deltaP_area_change[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='ipopt',
                   optarg={'tol': 1e-6}):
        '''
        WaterPipe 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={'tol': 1e-6})
            solver : str indicating whcih solver to use during
                     initialization (default = 'ipopt')

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

        opt = SolverFactory(solver)
        opt.options = optarg

        flags = blk.control_volume.initialize(outlvl=outlvl + 1,
                                              optarg=optarg,
                                              solver=solver,
                                              state_args=state_args)
        init_log.info_high("Initialization Step 1 Complete.")
        # Fix outlet enthalpy and pressure
        for t in blk.flowsheet().config.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().config.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)
        init_log.info("Initialization Complete.")

    def calculate_scaling_factors(self):
        pass