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 = {
                "dynamic": False,
                "inlet_list": ["steam", "drain"],
                "property_package": config.property_package,
                "momentum_mixing_type": MomentumMixingType.none,
            }
            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.SMX = 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.SDS = Arc(source=self.desuperheat.outlet_1,
                               destination=self.drain_mix.steam)
            else:
                self.SDS = Arc(source=self.desuperheat.outlet_1,
                               destination=self.condense.inlet_1)
            self.FW2 = 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.FW1 = Arc(source=self.cooling.outlet_2,
                           destination=self.condense.inlet_2)
            self.SC = Arc(source=self.condense.outlet_1,
                          destination=self.cooling.inlet_1)

        TransformationFactory("network.expand_arcs").apply_to(self)
Exemple #2
0
def test_mixer2():
    m = pyo.ConcreteModel()
    m.fs = idaes.core.FlowsheetBlock(default={"dynamic": False})
    m.fs.properties = iapws95.Iapws95ParameterBlock()
    m.fs.unit = HelmMixer(
        default={
            "momentum_mixing_type": MomentumMixingType.equality,
            "property_package": m.fs.properties,
            "inlet_list": ["i1", "i2", "i3"]
        })

    Fin1 = 1.1e4  # mol/s
    hin1 = 4000  # J/mol
    Pin1 = 1.2e5  # Pa
    Fin2 = 1e4  # mol/s
    hin2 = 5000  # J/mol
    Pin2 = 2e5  # Pa
    Fin3 = 1.3e4  # mol/s
    hin3 = 6000  # J/mol
    Pin3 = 3e5  # Pa
    Pout = 1.5e5  # Pa

    m.fs.unit.i1.flow_mol[0].fix(Fin1)
    m.fs.unit.i1.enth_mol[0].fix(hin1)
    m.fs.unit.i1.pressure[0] = Pin1
    m.fs.unit.i2.flow_mol[0].fix(Fin2)
    m.fs.unit.i2.enth_mol[0].fix(hin2)
    m.fs.unit.i2.pressure[0] = Pin2
    m.fs.unit.i3.flow_mol[0].fix(Fin3)
    m.fs.unit.i3.enth_mol[0].fix(hin3)
    m.fs.unit.i3.pressure[0] = Pin3
    m.fs.unit.outlet.pressure[0].fix(Pout)

    m.fs.unit.initialize()
    Fout = Fin1 + Fin2 + Fin3
    hout = (hin1 * Fin1 + hin2 * Fin2 + hin3 * Fin3) / Fout

    assert pyo.value(m.fs.unit.outlet.flow_mol[0]) == pytest.approx(Fout,
                                                                    rel=1e-7)
    assert pyo.value(m.fs.unit.outlet.enth_mol[0]) == pytest.approx(hout,
                                                                    rel=1e-7)
    assert pyo.value(m.fs.unit.outlet.pressure[0]) == pytest.approx(Pout,
                                                                    rel=1e-7)
    assert pyo.value(m.fs.unit.i1.pressure[0]) == pytest.approx(Pout, rel=1e-7)
    assert pyo.value(m.fs.unit.i2.pressure[0]) == pytest.approx(Pout, rel=1e-7)
    assert pyo.value(m.fs.unit.i3.pressure[0]) == pytest.approx(Pout, rel=1e-7)
def create_model():
    """Create the flowsheet and add unit models. Fixing model inputs is done
    in a separate function to try to keep this fairly clean and easy to follow.

    Args:
        None

    Returns:
        (ConcreteModel) Steam cycle model
    """
    ############################################################################
    #  Flowsheet and Properties                                                #
    ############################################################################
    m = pyo.ConcreteModel(name="Steam Cycle Model")
    m.fs = FlowsheetBlock(default={"dynamic":
                                   False})  # Add steady state flowsheet

    # A physical property parameter block for IAPWS-95 with pressure and enthalpy
    # (PH) state variables.  Usually pressure and enthalpy state variables are
    # more robust especially when the phases are unknown.
    m.fs.prop_water = iapws95.Iapws95ParameterBlock(
        default={"phase_presentation": iapws95.PhaseType.MIX})

    # A physical property parameter block with temperature, pressure and vapor
    # fraction (TPx) state variables. There are a few instances where the vapor
    # fraction is known and the temperature and pressure state variables are
    # preferable.
    m.fs.prop_water_tpx = iapws95.Iapws95ParameterBlock(
        default={
            "phase_presentation": iapws95.PhaseType.LG,
            "state_vars": iapws95.StateVars.TPX,
        })
    ############################################################################
    #  Turbine with fill-in reheat constraints                                 #
    ############################################################################
    # The TurbineMultistage class allows creation of the full turbine model by
    # providing several configuration options, including: throttle valves;
    # high-, intermediate-, and low-pressure sections; steam extractions; and
    # pressure driven flow.  See the IDAES documentation for details.
    m.fs.turb = HelmTurbineMultistage(
        default={
            "property_package": m.fs.prop_water,
            "num_parallel_inlet_stages": 4,  # number of admission arcs
            "num_hp": 7,  # number of high-pressure stages
            "num_ip": 10,  # number of intermediate-pressure stages
            "num_lp": 11,  # number of low-pressure stages
            "hp_split_locations": [4, 7],  # hp steam extraction locations
            "ip_split_locations": [5, 10],  # ip steam extraction locations
            "lp_split_locations": [4, 8, 10, 11
                                   ],  # lp steam extraction locations
            "hp_disconnect": [7],  # disconnect hp from ip to insert reheater
            "ip_split_num_outlets": {
                10: 3
            },  # number of split streams (default is 2)
        })
    # This model is only the steam cycle, and the reheater is part of the boiler.
    # To fill in the reheater gap, a few constraints for the flow, pressure drop,
    # and outlet temperature are added. A detailed boiler model can be coupled later.
    #
    # hp_split[7] is the splitter directly after the last HP stage.  The splitter
    # outlet "outlet_1" is always taken to be the main steam flow through the turbine.
    # When the turbine model was instantiated the stream from the HP section to the IP
    # section was omitted, so the reheater could be inserted.

    # The flow constraint sets flow from outlet_1 of the splitter equal to
    # flow into the IP turbine.
    @m.fs.turb.Constraint(m.fs.time)
    def constraint_reheat_flow(b, t):
        return b.ip_stages[1].inlet.flow_mol[t] == b.hp_split[
            7].outlet_1.flow_mol[t]

    # Create a variable for pressure change in the reheater (assuming
    # reheat_delta_p should be negative).
    m.fs.turb.reheat_delta_p = pyo.Var(m.fs.time,
                                       initialize=0,
                                       units=pyo.units.Pa)

    # Add a constraint to calculate the IP section inlet pressure based on the
    # pressure drop in the reheater and the outlet pressure of the HP section.
    @m.fs.turb.Constraint(m.fs.time)
    def constraint_reheat_press(b, t):
        return (b.ip_stages[1].inlet.pressure[t] ==
                b.hp_split[7].outlet_1.pressure[t] + b.reheat_delta_p[t])

    # Create a variable for reheat temperature and fix it to the desired reheater
    # outlet temperature
    m.fs.turb.reheat_out_T = pyo.Var(m.fs.time,
                                     initialize=866,
                                     units=pyo.units.K)

    # Create a constraint for the IP section inlet temperature.
    @m.fs.turb.Constraint(m.fs.time)
    def constraint_reheat_temp(b, t):
        return (b.ip_stages[1].control_volume.properties_in[t].temperature ==
                b.reheat_out_T[t])

    ############################################################################
    #  Add Condenser/hotwell/condensate pump                                   #
    ############################################################################
    # Add a mixer for all the streams coming into the condenser.  In this case the
    # main steam, and the boiler feed pump turbine outlet go to the condenser
    m.fs.condenser_mix = HelmMixer(
        default={
            "momentum_mixing_type": MomentumMixingType.none,
            "inlet_list": ["main", "bfpt"],
            "property_package": m.fs.prop_water,
        })
    # The pressure in the mixer comes from the connection to the condenser.  All
    # the streams coming in and going out of the mixer are equal, but we created
    # the mixer with no calculation for the unit pressure. Here a constraint that
    # specifies that the mixer pressure is equal to the main steam pressure is
    # added.  There is also a constraint that specifies the that BFP turbine outlet
    # pressure is the same as the condenser pressure.  Combined with the stream
    # connections between units, these constraints effectively specify that the
    # mixer inlet and outlet streams all have the same pressure.
    @m.fs.condenser_mix.Constraint(m.fs.time)
    def mixer_pressure_constraint(b, t):
        return b.main_state[t].pressure == b.mixed_state[t].pressure

    # The condenser model uses the physical property model with TPx state
    # variables, while the rest of the model uses PH state variables. To
    # translate between the two property calculations, an extra port is added to
    # the mixer which contains temperature, pressure, and vapor fraction
    # quantities.
    m.fs.condenser_mix.outlet_tpx = Port(
        initialize={
            "flow_mol":
            pyo.Reference(m.fs.condenser_mix.mixed_state[:].flow_mol),
            "temperature":
            pyo.Reference(m.fs.condenser_mix.mixed_state[:].temperature),
            "pressure":
            pyo.Reference(m.fs.condenser_mix.mixed_state[:].pressure),
            "vapor_frac":
            pyo.Reference(m.fs.condenser_mix.mixed_state[:].vapor_frac),
        })

    # Add the heat exchanger model for the condenser.
    m.fs.condenser = HeatExchanger(
        default={
            "delta_temperature_callback": delta_temperature_underwood_callback,
            "shell": {
                "property_package": m.fs.prop_water_tpx
            },
            "tube": {
                "property_package": m.fs.prop_water
            },
        })
    m.fs.condenser.delta_temperature_out.fix(5)

    # Everything condenses so the saturation pressure determines the condenser
    # pressure. Deactivate the constraint that is used in the TPx version vapor
    # fraction constraint and fix vapor fraction to 0.
    m.fs.condenser.shell.properties_out[:].eq_complementarity.deactivate()
    m.fs.condenser.shell.properties_out[:].vapor_frac.fix(0)

    # There is some subcooling in the condenser, so we assume the condenser
    # pressure is actually going to be slightly higher than the saturation
    # pressure.
    m.fs.condenser.pressure_over_sat = pyo.Var(
        m.fs.time,
        initialize=500,
        doc="Pressure added to Psat in the condeser. This is to account for"
        "some subcooling. (Pa)",
        units=pyo.units.Pa)
    # Add a constraint for condenser pressure
    @m.fs.condenser.Constraint(m.fs.time)
    def eq_pressure(b, t):
        return (b.shell.properties_out[t].pressure ==
                b.shell.properties_out[t].pressure_sat +
                b.pressure_over_sat[t])

    # Extra port on condenser to hook back up to pressure-enthalpy properties
    m.fs.condenser.outlet_1_ph = Port(
        initialize={
            "flow_mol":
            pyo.Reference(m.fs.condenser.shell.properties_out[:].flow_mol),
            "pressure":
            pyo.Reference(m.fs.condenser.shell.properties_out[:].pressure),
            "enth_mol":
            pyo.Reference(m.fs.condenser.shell.properties_out[:].enth_mol),
        })

    # Add the condenser hotwell.  In steady state a mixer will work.  This is
    # where makeup water is added if needed.
    m.fs.hotwell = HelmMixer(
        default={
            "momentum_mixing_type": MomentumMixingType.none,
            "inlet_list": ["condensate", "makeup"],
            "property_package": m.fs.prop_water,
        })

    # The hotwell is assumed to be at the same pressure as the condenser.
    @m.fs.hotwell.Constraint(m.fs.time)
    def mixer_pressure_constraint(b, t):
        return b.condensate_state[t].pressure == b.mixed_state[t].pressure

    # Condensate pump (Use compressor model, since it is more robust if vapor form)
    m.fs.cond_pump = HelmIsentropicCompressor(
        default={"property_package": m.fs.prop_water})
    ############################################################################
    #  Add low pressure feedwater heaters                                      #
    ############################################################################
    # All the feedwater heater sections will be set to use the Underwood
    # approximation for LMTD, so create the fwh_config dict to make the config
    # slightly cleaner
    fwh_config = {
        "delta_temperature_callback": delta_temperature_underwood_callback
    }

    # The feedwater heater model allows feedwater heaters with a desuperheat,
    # condensing, and subcooling section to be added an a reasonably simple way.
    # See the IDAES documentation for more information of configuring feedwater
    # heaters
    m.fs.fwh1 = FWH0D(
        default={
            "has_desuperheat": False,
            "has_drain_cooling": False,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "condense": fwh_config,
        })
    # pump for fwh1 condensate, to pump it ahead and mix with feedwater
    m.fs.fwh1_pump = HelmIsentropicCompressor(
        default={"property_package": m.fs.prop_water})
    # Mix the FWH1 drain back into the feedwater
    m.fs.fwh1_return = HelmMixer(
        default={
            "momentum_mixing_type": MomentumMixingType.none,
            "inlet_list": ["feedwater", "fwh1_drain"],
            "property_package": m.fs.prop_water,
        })

    # Set the mixer pressure to the feedwater pressure
    @m.fs.fwh1_return.Constraint(m.fs.time)
    def mixer_pressure_constraint(b, t):
        return b.feedwater_state[t].pressure == b.mixed_state[t].pressure

    # Add the rest of the low pressure feedwater heaters
    m.fs.fwh2 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    m.fs.fwh3 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    m.fs.fwh4 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": False,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    ############################################################################
    #  Add deaerator and boiler feed pump (BFP)                                #
    ############################################################################
    # The deaerator is basically an open tank with multiple inlets.  For steady-
    # state, a mixer model is sufficient.
    m.fs.fwh5_da = HelmMixer(
        default={
            "momentum_mixing_type": MomentumMixingType.none,
            "inlet_list": ["steam", "drain", "feedwater"],
            "property_package": m.fs.prop_water,
        })

    @m.fs.fwh5_da.Constraint(m.fs.time)
    def mixer_pressure_constraint(b, t):
        # Not sure about deaerator pressure, so assume same as feedwater inlet
        return b.feedwater_state[t].pressure == b.mixed_state[t].pressure

    # Add the boiler feed pump and boiler feed pump turbine
    m.fs.bfp = HelmIsentropicCompressor(
        default={"property_package": m.fs.prop_water})

    m.fs.bfpt = HelmTurbineStage(default={"property_package": m.fs.prop_water})

    # The boiler feed pump outlet pressure is the same as the condenser
    @m.fs.Constraint(m.fs.time)
    def constraint_out_pressure(b, t):
        return (b.bfpt.control_volume.properties_out[t].pressure ==
                b.condenser.shell.properties_out[t].pressure)

    # Instead of specifying a fixed efficiency, specify that the steam is just
    # starting to condense at the outlet of the boiler feed pump turbine.  This
    # ensures approximately the right behavior in the turbine.  With a fixed
    # efficiency, depending on the conditions you can get odd things like steam
    # fully condensing in the turbine.
    @m.fs.Constraint(m.fs.time)
    def constraint_out_enthalpy(b, t):
        return (
            b.bfpt.control_volume.properties_out[t].enth_mol ==
            b.bfpt.control_volume.properties_out[t].enth_mol_sat_phase["Vap"] -
            200 * pyo.units.J / pyo.units.mol)

    # The boiler feed pump power is the same as the power generated by the
    # boiler feed pump turbine. This constraint determines the steam flow to the
    # BFP turbine. The turbine work is negative for power out, while pump work
    # is positive for power in.
    @m.fs.Constraint(m.fs.time)
    def constraint_bfp_power(b, t):
        return 0 == b.bfp.control_volume.work[t] + b.bfpt.control_volume.work[t]

    ############################################################################
    #  Add high pressure feedwater heaters                                     #
    ############################################################################
    m.fs.fwh6 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    m.fs.fwh7 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": True,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    m.fs.fwh8 = FWH0D(
        default={
            "has_desuperheat": True,
            "has_drain_cooling": True,
            "has_drain_mixer": False,
            "property_package": m.fs.prop_water,
            "desuperheat": fwh_config,
            "cooling": fwh_config,
            "condense": fwh_config,
        })
    ############################################################################
    #  Additional Constraints/Expressions                                      #
    ############################################################################

    # Add a few constraints to allow a for complete plant results despite the
    # lack of a detailed boiler model.

    # Boiler pressure drop
    m.fs.boiler_pressure_drop_fraction = pyo.Var(
        m.fs.time,
        initialize=0.01,
        doc="Fraction of pressure lost from boiler feed pump and turbine inlet",
    )

    @m.fs.Constraint(m.fs.time)
    def boiler_pressure_drop(b, t):
        return (m.fs.bfp.control_volume.properties_out[t].pressure *
                (1 - b.boiler_pressure_drop_fraction[t]) ==
                m.fs.turb.inlet_split.mixed_state[t].pressure)

    # Again, since the boiler is missing, set the flow of steam into the turbine
    # equal to the flow of feedwater out of the last feedwater heater.
    @m.fs.Constraint(m.fs.time)
    def close_flow(b, t):
        return (m.fs.bfp.control_volume.properties_out[t].flow_mol ==
                m.fs.turb.inlet_split.mixed_state[t].flow_mol)

    # Calculate the amount of heat that is added in the boiler, including the
    # reheater.
    @m.fs.Expression(m.fs.time)
    def boiler_heat(b, t):
        return (b.turb.inlet_split.mixed_state[t].enth_mol *
                b.turb.inlet_split.mixed_state[t].flow_mol -
                b.fwh8.desuperheat.tube.properties_out[t].enth_mol *
                b.fwh8.desuperheat.tube.properties_out[t].flow_mol +
                b.turb.ip_stages[1].control_volume.properties_in[t].enth_mol *
                b.turb.ip_stages[1].control_volume.properties_in[t].flow_mol -
                b.turb.hp_split[7].outlet_1.enth_mol[t] *
                b.turb.hp_split[7].outlet_1.flow_mol[t])

    # Calculate the efficiency of the steam cycle.  This doesn't account for
    # heat loss in the boiler, so actual plant efficiency would be lower.
    @m.fs.Expression(m.fs.time)
    def steam_cycle_eff(b, t):
        return -100 * b.turb.power[t] / b.boiler_heat[t]

    ############################################################################
    ##  Create the stream Arcs                                                ##
    ############################################################################

    ############################################################################
    #  Connect turbine and condenser units                                     #
    ############################################################################
    m.fs.EXHST_MAIN = Arc(source=m.fs.turb.outlet_stage.outlet,
                          destination=m.fs.condenser_mix.main)
    m.fs.condenser_mix_to_condenser = Arc(source=m.fs.condenser_mix.outlet_tpx,
                                          destination=m.fs.condenser.inlet_1)
    m.fs.COND_01 = Arc(source=m.fs.condenser.outlet_1_ph,
                       destination=m.fs.hotwell.condensate)
    m.fs.COND_02 = Arc(source=m.fs.hotwell.outlet,
                       destination=m.fs.cond_pump.inlet)
    ############################################################################
    #  Low pressure FWHs                                                       #
    ############################################################################
    m.fs.EXTR_LP11 = Arc(source=m.fs.turb.lp_split[11].outlet_2,
                         destination=m.fs.fwh1.drain_mix.steam)
    m.fs.COND_03 = Arc(source=m.fs.cond_pump.outlet,
                       destination=m.fs.fwh1.condense.inlet_2)
    m.fs.FWH1_DRN1 = Arc(source=m.fs.fwh1.condense.outlet_1,
                         destination=m.fs.fwh1_pump.inlet)
    m.fs.FWH1_DRN2 = Arc(source=m.fs.fwh1_pump.outlet,
                         destination=m.fs.fwh1_return.fwh1_drain)
    m.fs.FW01A = Arc(source=m.fs.fwh1.condense.outlet_2,
                     destination=m.fs.fwh1_return.feedwater)
    # fwh2
    m.fs.FW01B = Arc(source=m.fs.fwh1_return.outlet,
                     destination=m.fs.fwh2.cooling.inlet_2)
    m.fs.FWH2_DRN = Arc(source=m.fs.fwh2.cooling.outlet_1,
                        destination=m.fs.fwh1.drain_mix.drain)
    m.fs.EXTR_LP10 = Arc(
        source=m.fs.turb.lp_split[10].outlet_2,
        destination=m.fs.fwh2.desuperheat.inlet_1,
    )
    # fwh3
    m.fs.FW02 = Arc(source=m.fs.fwh2.desuperheat.outlet_2,
                    destination=m.fs.fwh3.cooling.inlet_2)
    m.fs.FWH3_DRN = Arc(source=m.fs.fwh3.cooling.outlet_1,
                        destination=m.fs.fwh2.drain_mix.drain)
    m.fs.EXTR_LP8 = Arc(source=m.fs.turb.lp_split[8].outlet_2,
                        destination=m.fs.fwh3.desuperheat.inlet_1)
    # fwh4
    m.fs.FW03 = Arc(source=m.fs.fwh3.desuperheat.outlet_2,
                    destination=m.fs.fwh4.cooling.inlet_2)
    m.fs.FWH4_DRN = Arc(source=m.fs.fwh4.cooling.outlet_1,
                        destination=m.fs.fwh3.drain_mix.drain)
    m.fs.EXTR_LP4 = Arc(source=m.fs.turb.lp_split[4].outlet_2,
                        destination=m.fs.fwh4.desuperheat.inlet_1)
    ############################################################################
    #  FWH5 (Deaerator) and boiler feed pump (BFP)                             #
    ############################################################################
    m.fs.FW04 = Arc(source=m.fs.fwh4.desuperheat.outlet_2,
                    destination=m.fs.fwh5_da.feedwater)
    m.fs.EXTR_IP10 = Arc(source=m.fs.turb.ip_split[10].outlet_2,
                         destination=m.fs.fwh5_da.steam)
    m.fs.FW05A = Arc(source=m.fs.fwh5_da.outlet, destination=m.fs.bfp.inlet)
    m.fs.EXTR_BFPT_A = Arc(source=m.fs.turb.ip_split[10].outlet_3,
                           destination=m.fs.bfpt.inlet)
    m.fs.EXHST_BFPT = Arc(source=m.fs.bfpt.outlet,
                          destination=m.fs.condenser_mix.bfpt)
    ############################################################################
    #  High-pressure feedwater heaters                                         #
    ############################################################################
    # fwh6
    m.fs.FW05B = Arc(source=m.fs.bfp.outlet,
                     destination=m.fs.fwh6.cooling.inlet_2)
    m.fs.FWH6_DRN = Arc(source=m.fs.fwh6.cooling.outlet_1,
                        destination=m.fs.fwh5_da.drain)
    m.fs.EXTR_IP5 = Arc(source=m.fs.turb.ip_split[5].outlet_2,
                        destination=m.fs.fwh6.desuperheat.inlet_1)
    # fwh7
    m.fs.FW06 = Arc(source=m.fs.fwh6.desuperheat.outlet_2,
                    destination=m.fs.fwh7.cooling.inlet_2)
    m.fs.FWH7_DRN = Arc(source=m.fs.fwh7.cooling.outlet_1,
                        destination=m.fs.fwh6.drain_mix.drain)
    m.fs.EXTR_HP7 = Arc(source=m.fs.turb.hp_split[7].outlet_2,
                        destination=m.fs.fwh7.desuperheat.inlet_1)
    # fwh8
    m.fs.FW07 = Arc(source=m.fs.fwh7.desuperheat.outlet_2,
                    destination=m.fs.fwh8.cooling.inlet_2)
    m.fs.FWH8_DRN = Arc(source=m.fs.fwh8.cooling.outlet_1,
                        destination=m.fs.fwh7.drain_mix.drain)
    m.fs.EXTR_HP4 = Arc(source=m.fs.turb.hp_split[4].outlet_2,
                        destination=m.fs.fwh8.desuperheat.inlet_1)

    ############################################################################
    # Turn the Arcs into constraints and return the model                      #
    ############################################################################
    pyo.TransformationFactory("network.expand_arcs").apply_to(m.fs)
    return m
Exemple #4
0
    def build(self):
        super().build()
        config = self.config
        unit_cfg = {  # general unit model config
            "dynamic": config.dynamic,
            "has_holdup": config.has_holdup,
            "property_package": config.property_package,
            "property_package_args": config.property_package_args,
        }
        ni = self.config.num_parallel_inlet_stages
        inlet_idx = self.inlet_stage_idx = pyo.RangeSet(ni)

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

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

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

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

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

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

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

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

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

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

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

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

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

            return _rule

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            return _rule

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        for t, c in self.power_eqn.items():
            power_scale = iscale.get_scaling_factor(self.power[t],
                                                    default=1,
                                                    warning=True)
            # Set power equation scale factor
            iscale.constraint_scaling_transform(c,
                                                power_scale,
                                                overwrite=False)
class FWH0DDynamicData(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 = {"dynamic": False,
                       "inlet_list": ["steam", "drain"],
                       "property_package": config.property_package,
                       "momentum_mixing_type": MomentumMixingType.none,
                       }
            self.drain_mix = Mixer(default=mix_cfg)

            @self.drain_mix.Constraint(self.drain_mix.flowsheet().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.SMX = 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.SDS = Arc(
                    source=self.desuperheat.outlet_1,
                    destination=self.drain_mix.steam
                )
            else:
                self.SDS = Arc(
                    source=self.desuperheat.outlet_1,
                    destination=self.condense.inlet_1
                )
            self.FW2 = 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.FW1 = Arc(
                source=self.cooling.outlet_2, destination=self.condense.inlet_2
            )
            self.SC = Arc(
                source=self.condense.outlet_1, destination=self.cooling.inlet_1
            )

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

    def set_initial_condition(self):
        # currently assume steady-state for desuperheater
        if self.config.dynamic is True:
            if self.condense.config.has_holdup is True:
                self.condense.tube.material_accumulation[:, :, :].value = 0
                self.condense.tube.energy_accumulation[:, :].value = 0
                self.condense.tube.material_accumulation[0, :, :].fix(0)
                self.condense.tube.energy_accumulation[0, :].fix(0)
                self.condense.shell.material_accumulation[:, :, :].value = 0
                self.condense.shell.energy_accumulation[:, :].value = 0
                self.condense.shell.material_accumulation[0, :, :].fix(0)
                self.condense.shell.energy_accumulation[0, :].fix(0)
            if self.config.has_drain_cooling is True:
                if self.cooling.config.has_holdup is True:
                    self.cooling.tube.material_accumulation[:, :, :].value = 0
                    self.cooling.tube.energy_accumulation[:, :].value = 0
                    self.cooling.tube.material_accumulation[0, :, :].fix(0)
                    self.cooling.tube.energy_accumulation[0, :].fix(0)
                    self.cooling.shell.material_accumulation[:, :, :].value = 0
                    self.cooling.shell.energy_accumulation[:, :].value = 0
                    self.cooling.shell.material_accumulation[0, :, :].fix(0)
                    self.cooling.shell.energy_accumulation[0, :].fix(0)

    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 aproaches which can be implimented if needed.

        # initialize desuperheat if any
        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()
        # 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)

        # Create solver
        opt = get_solver(kwargs.get("solver"), 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)

    def calculate_scaling_factors(self):
        if hasattr(self, "mixer_pressure_constraint"):
            for t, c in self.mixer_pressure_constraint.items():
                sf = iscale.get_scaling_factor(
                    self.steam_state[t].pressure, default=1, warning=True)
                iscale.constraint_scaling_transform(c, sf, overwrite=False)
        if hasattr(self, "pressure_change_total_eqn"):
            for t, c in self.pressure_change_total_eqn.items():
                sf = iscale.get_scaling_factor(
                    self.steam_state[t].pressure, default=1, warning=True)
                iscale.constraint_scaling_transform(c, sf, overwrite=False)
Exemple #7
0
def add_unit_models(m):
    fs = m.fs_main.fs_blr
    prop_water = m.fs_main.prop_water
    prop_gas = m.fs_main.prop_gas

    fs.num_mills = pyo.Var()
    fs.num_mills.fix(4)

    # 14 waterwall zones
    fs.ww_zones = pyo.RangeSet(14)

    # boiler based on surrogate
    fs.aBoiler = BoilerSurrogate(default={"dynamic": False,
                               "side_1_property_package": prop_gas,
                               "side_2_property_package": prop_gas,
                               "has_heat_transfer": False,
                               "has_pressure_change": False,
                               "has_holdup": False})

    # model a drum by a WaterFlash, a Mixer and a Drum model
    fs.aFlash = WaterFlash(default={"dynamic": False,
                               "property_package": prop_water,
                               "has_phase_equilibrium": False,
                               "has_heat_transfer": False,
                               "has_pressure_change": False})
    fs.aMixer = HelmMixer(default={"dynamic": False,
                              "property_package": prop_water,
                              "momentum_mixing_type": MomentumMixingType.equality,
                              "inlet_list": ["FeedWater", "SatWater"]})
    fs.aDrum = Drum1D(default={"property_package": prop_water,
                               "has_holdup": True,
                               "has_heat_transfer": True,
                               "has_pressure_change": True,
                               "finite_elements": 4,
                               "inside_diameter": 1.778,
                               "thickness": 0.127})
    fs.blowdown_split = HelmSplitter(
        default={
            "dynamic": False,
            "property_package": prop_water,
            "outlet_list": ["FW_Downcomer", "FW_Blowdown"],
        }
    )
    # downcomer
    fs.aDowncomer = Downcomer(default={
                               "dynamic": False,
                               "property_package": prop_water,
                               "has_holdup": True,
                               "has_heat_transfer": True,
                               "has_pressure_change": True})

    # 14 WaterwallSection units
    fs.Waterwalls = WaterwallSection(fs.ww_zones,
                               default={
                               "has_holdup": True,
                               "property_package": prop_water,
                               "has_equilibrium_reactions": False,
                               "has_heat_of_reaction": False,
                               "has_heat_transfer": True,
                               "has_pressure_change": True})

    # roof superheater
    fs.aRoof = SteamHeater(default={
                               "dynamic": False,
                               "property_package": prop_water,
                               "has_holdup": True,
                               "has_equilibrium_reactions": False,
                               "has_heat_of_reaction": False,
                               "has_heat_transfer": True,
                               "has_pressure_change": True,
                               "single_side_only" : True})

    # platen superheater
    fs.aPlaten = SteamHeater(default={
                               "dynamic": False,
                               "property_package": prop_water,
                               "has_holdup": True,
                               "has_equilibrium_reactions": False,
                               "has_heat_of_reaction": False,
                               "has_heat_transfer": True,
                               "has_pressure_change": True,
                               "single_side_only" : False})

    # 1st reheater
    fs.aRH1 = HeatExchangerCrossFlow2D(default={
                               "tube_side":{"property_package": prop_water, "has_holdup": False,
                                            "has_pressure_change": True},
                               "shell_side":{"property_package": prop_gas, "has_holdup": False,
                                             "has_pressure_change": True},
                               "finite_elements": 4,
                               "flow_type": "counter_current",
                               "tube_arrangement": "in-line",
                               "tube_side_water_phase": "Vap",
                               "has_radiation": True,
                               "radial_elements": 5,
                               "inside_diameter": 2.202*0.0254,
                               "thickness": 0.149*0.0254})

    # 2nd reheater
    fs.aRH2 = HeatExchangerCrossFlow2D(default={
                               "tube_side":{"property_package": prop_water, "has_holdup": False,
                                            "has_pressure_change": True},
                               "shell_side":{"property_package": prop_gas, "has_holdup": False,
                                             "has_pressure_change": True},
                               "finite_elements": 2,
                               "flow_type": "counter_current",
                               "tube_arrangement": "in-line",
                               "tube_side_water_phase": "Vap",
                               "has_radiation": True,
                               "radial_elements": 5,
                               "inside_diameter": 2.217*0.0254,
                               "thickness": 0.1415*0.0254})

    # primary superheater
    fs.aPSH = HeatExchangerCrossFlow2D(default={
                               "tube_side":{"property_package": prop_water, "has_holdup": False,
                                            "has_pressure_change": True},
                               "shell_side":{"property_package": prop_gas, "has_holdup": False,
                                             "has_pressure_change": True},
                               "finite_elements": 5,
                               "flow_type": "counter_current",
                               "tube_arrangement": "in-line",
                               "tube_side_water_phase": "Vap",
                               "has_radiation": True,
                               "radial_elements": 5,
                               "inside_diameter": 1.45*0.0254,
                               "thickness": 0.15*0.0254})

    # economizer
    fs.aECON = HeatExchangerCrossFlow2D(default={
                               "tube_side":{"property_package": prop_water, "has_holdup": False,
                                            "has_pressure_change": True},
                               "shell_side":{"property_package": prop_gas, "has_holdup": False,
                                             "has_pressure_change": True},
                               "finite_elements": 5,
                               "flow_type": "counter_current",
                               "tube_arrangement": "in-line",
                               "tube_side_water_phase": "Liq",
                               "has_radiation": False,
                               "radial_elements": 5,
                               "inside_diameter": 1.452*0.0254,
                               "thickness": 0.149*0.0254})

    # water pipe from economizer outlet to drum
    fs.aPipe = WaterPipe(default={
                               "dynamic": False,
                               "property_package": prop_water,
                               "has_holdup": True,
                               "has_heat_transfer": False,
                               "has_pressure_change": True,
                               "water_phase": 'Liq',
                               "contraction_expansion_at_end": 'None'})

    # a mixer to mix hot primary air with tempering air
    fs.Mixer_PA = Mixer(
        default={
            "dynamic": False,
            "property_package": prop_gas,
            "momentum_mixing_type": MomentumMixingType.equality,
            "inlet_list": ["PA_inlet", "TA_inlet"],
        }
    )

    # attemperator for main steam before platen SH
    fs.Attemp = HelmMixer(
        default={
            "dynamic": False,
            "property_package": prop_water,
            "momentum_mixing_type": MomentumMixingType.equality,
            "inlet_list": ["Steam_inlet", "Water_inlet"],
        }
    )

    # air preheater as three-stream heat exchanger with heat loss to ambient,
    #     side_1: flue gas
    #     side_2:PA (priamry air?)
    #     side_3:PA (priamry air?)
    fs.aAPH = HeatExchangerWith3Streams(
        default={"dynamic": False,
            "side_1_property_package": prop_gas,
            "side_2_property_package": prop_gas,
            "side_3_property_package": prop_gas,
            "has_heat_transfer": True,
            "has_pressure_change": True,
            "has_holdup": False,
            "flow_type_side_2": "counter-current",
            "flow_type_side_3": "counter-current",
        }
    )
    return m