Esempio n. 1
0
def build_heat_exchanger():
    m = ConcreteModel()
    m.fs = FlowsheetBlock(default={"dynamic": False})
    m.fs.properties = iapws95_ph.Iapws95ParameterBlock()
    m.fs.heat_exchanger = HeatExchanger(default={
        "side_1":{"property_package": m.fs.properties},
        "side_2":{"property_package": m.fs.properties}})
    return m
Esempio n. 2
0
    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)
Esempio n. 3
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):
        config = self.config  # sorter 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 initilization 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()
        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
        outlvl = kwargs.get("outlvl", 0)
        opt = SolverFactory(kwargs.get("solver", "ipopt"))
        opt.options = kwargs.get("oparg", {})
        tee = True if outlvl >= 3 else False
        assert (degrees_of_freedom(self) == 0)
        results = opt.solve(self, tee=tee)
        if results.solver.termination_condition == TerminationCondition.optimal:
            if outlvl >= 2:
                _log.info('{} Initialization Complete.'.format(self.name))
        else:
            _log.warning('{} Initialization Failed.'.format(self.name))

        from_json(self, sd=istate, wts=sp)
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.LG})

    # 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 = TurbineMultistage(
        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)

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

    # 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 = Mixer(
        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. The first step is to add references to the temperature and
    # vapor fraction expressions in the IAPWS-95 property block. The references
    # are used to handle time indexing in the ports by using the property blocks
    # time index to create references that appear to be time indexed variables.
    # These references mirror the references created by the framework automatically
    # for the existing ports.
    m.fs.condenser_mix._outlet_temperature_ref = pyo.Reference(
        m.fs.condenser_mix.mixed_state[:].temperature)
    m.fs.condenser_mix._outlet_vapor_fraction_ref = pyo.Reference(
        m.fs.condenser_mix.mixed_state[:].vapor_frac)
    # Add the new port with the state information that needs to go to the
    # condenser
    m.fs.condenser_mix.outlet_tpx = Port(
        initialize={
            "flow_mol": m.fs.condenser_mix._outlet_flow_mol_ref,
            "temperature": m.fs.condenser_mix._outlet_temperature_ref,
            "pressure": m.fs.condenser_mix._outlet_pressure_ref,
            "vapor_frac": m.fs.condenser_mix._outlet_vapor_fraction_ref,
        })

    # 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)",
    )
    # 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_enth_mol_ref = pyo.Reference(
        m.fs.condenser.shell.properties_out[:].enth_mol)
    m.fs.condenser.outlet_1_ph = Port(
        initialize={
            "flow_mol": m.fs.condenser._outlet_1_flow_mol_ref,
            "pressure": m.fs.condenser._outlet_1_pressure_ref,
            "enth_mol": m.fs.condenser._outlet_1_enth_mol_ref,
        })

    # Add the condenser hotwell.  In steady state a mixer will work.  This is
    # where makeup water is added if needed.
    m.fs.hotwell = Mixer(
        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
    m.fs.cond_pump = PressureChanger(
        default={
            "property_package": m.fs.prop_water,
            "thermodynamic_assumption": ThermodynamicAssumption.pump,
        })
    ############################################################################
    #  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 = PressureChanger(
        default={
            "property_package": m.fs.prop_water,
            "thermodynamic_assumption": ThermodynamicAssumption.pump,
        })
    # Mix the FWH1 drain back into the feedwater
    m.fs.fwh1_return = Mixer(
        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 = Mixer(
        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 = PressureChanger(
        default={
            "property_package": m.fs.prop_water,
            "thermodynamic_assumption": ThermodynamicAssumption.pump,
        })
    m.fs.bfpt = PressureChanger(
        default={
            "property_package": m.fs.prop_water,
            "compressor": False,
            "thermodynamic_assumption": ThermodynamicAssumption.isentropic,
        })

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

    # 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