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)
def declare_heatx_unit(h): """ Declare all units of the heat exchanger network. Optional keyword arguments: --------------------------- :param h: the heat exchanger network :return: return the heat exchanger network """ h.fs = FlowsheetBlock(default={"dynamic": False}) h.fs.thermo_params = thermo_props.MethaneParameterBlock() h.fs.Split2 = Separator(default={ "dynamic": False, "num_outlets": 3, "property_package": h.fs.thermo_params }) h.fs.HX1a = HeatExchanger( default={ "dynamic": False, "delta_temperature_callback": delta_temperature_amtd_callback, "shell": { "property_package": h.fs.thermo_params }, "tube": { "property_package": h.fs.thermo_params } }) h.fs.HX2a = HeatExchanger( default={ "dynamic": False, "delta_temperature_callback": delta_temperature_amtd_callback, "shell": { "property_package": h.fs.thermo_params }, "tube": { "property_package": h.fs.thermo_params } }) h.fs.HX2b = HeatExchanger( default={ "dynamic": False, "delta_temperature_callback": delta_temperature_amtd_callback, "shell": { "property_package": h.fs.thermo_params }, "tube": { "property_package": h.fs.thermo_params } }) h.fs.HX1b = Heater(default={"property_package": h.fs.thermo_params}) return h
def test_flowsheet_serializer_get_unit_model_type(): from idaes.core import MaterialBalanceType from idaes.generic_models.unit_models.pressure_changer import ( ThermodynamicAssumption, ) from idaes.generic_models.unit_models.heat_exchanger import ( delta_temperature_underwood_callback, ) from idaes.generic_models.properties import iapws95 from pyomo.environ import Set # flowsheet m = ConcreteModel(name="My Model") m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.prop_water = iapws95.Iapws95ParameterBlock( default={"phase_presentation": iapws95.PhaseType.LG}) # add & test scalar unit model m.fs.cond_pump = PressureChanger( default={ "property_package": m.fs.prop_water, "material_balance_type": MaterialBalanceType.componentTotal, "thermodynamic_assumption": ThermodynamicAssumption.pump, }) unit_type = FlowsheetSerializer.get_unit_model_type(m.fs.cond_pump) assert unit_type == "pressure_changer" # add & test indexed unit model m.set_fwh = Set(initialize=[1, 2, 3, 4, 6, 7, 8]) m.fs.fwh = HeatExchanger(m.set_fwh, default={ "delta_temperature_callback": delta_temperature_underwood_callback, "shell": { "property_package": m.fs.prop_water, "material_balance_type": MaterialBalanceType.componentTotal, "has_pressure_change": True, }, "tube": { "property_package": m.fs.prop_water, "material_balance_type": MaterialBalanceType.componentTotal, "has_pressure_change": True, }, }) unit_type = FlowsheetSerializer.get_unit_model_type(m.fs.fwh) assert unit_type == "heat_exchanger"
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
class FWH0DData(UnitModelBlockData): CONFIG = UnitModelBlockData.CONFIG() _define_feedwater_heater_0D_config(CONFIG) def build(self): super().build() config = self.config # sorter ref to config for less line splitting # All feedwater heaters have a condensing section _set_prop_pack(config.condense, config) self.condense = FWHCondensing0D(default=config.condense) # Add a mixer to add the drain stream from another feedwater heater if config.has_drain_mixer: mix_cfg = { # general unit model config "dynamic": config.dynamic, "has_holdup": config.has_holdup, "property_package": config.property_package, "property_package_args": config.property_package_args, "momentum_mixing_type": MomentumMixingType.none, "material_balance_type": MaterialBalanceType.componentTotal, "inlet_list": ["steam", "drain"], } self.drain_mix = Mixer(default=mix_cfg) @self.drain_mix.Constraint(self.drain_mix.flowsheet().config.time) def mixer_pressure_constraint(b, t): """ Constraint to set the drain mixer pressure to the pressure of the steam extracted from the turbine. The drain inlet should always be a higher pressure than the steam inlet. """ return b.steam_state[t].pressure == b.mixed_state[t].pressure # Connect the mixer to the condensing section inlet self.mix_out_arc = Arc(source=self.drain_mix.outlet, destination=self.condense.inlet_1) # Add a desuperheat section before the condensing section if config.has_desuperheat: _set_prop_pack(config.desuperheat, config) self.desuperheat = HeatExchanger(default=config.desuperheat) # set default area less than condensing section area, this will # almost always be overridden by the user fixing an area later self.desuperheat.area.value = 10 if config.has_drain_mixer: self.desuperheat_drain_arc = Arc( source=self.desuperheat.outlet_1, destination=self.drain_mix.steam) else: self.desuperheat_drain_arc = Arc( source=self.desuperheat.outlet_1, destination=self.condense.inlet_1) self.condense_out2_arc = Arc(source=self.condense.outlet_2, destination=self.desuperheat.inlet_2) # Add a drain cooling section after the condensing section if config.has_drain_cooling: _set_prop_pack(config.cooling, config) self.cooling = HeatExchanger(default=config.cooling) # set default area less than condensing section area, this will # almost always be overridden by the user fixing an area later self.cooling.area.value = 10 self.cooling_out2_arc = Arc(source=self.cooling.outlet_2, destination=self.condense.inlet_2) self.condense_out1_arc = Arc(source=self.condense.outlet_1, destination=self.cooling.inlet_1) TransformationFactory("network.expand_arcs").apply_to(self) def initialize(self, *args, **kwargs): outlvl = kwargs.get("outlvl", idaeslog.NOTSET) init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") config = self.config # shorter ref to config for less line splitting sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) # the initialization here isn't straight forward since the heat exchanger # may have 3 stages and they are countercurrent. For simplicity each # stage in initialized with the same cooling water inlet conditions then # the whole feedwater heater is solved together. There are more robust # approaches which can be implimented if the need arises. # initialize desuperheat if include if config.has_desuperheat: if config.has_drain_cooling: _set_port(self.desuperheat.inlet_2, self.cooling.inlet_2) else: _set_port(self.desuperheat.inlet_2, self.condense.inlet_2) self.desuperheat.initialize(*args, **kwargs) self.desuperheat.inlet_1.flow_mol.unfix() if config.has_drain_mixer: _set_port(self.drain_mix.steam, self.desuperheat.outlet_1) else: _set_port(self.condense.inlet_1, self.desuperheat.outlet_1) # fix the steam and fwh inlet for init self.desuperheat.inlet_1.fix() self.desuperheat.inlet_1.flow_mol.unfix() # unfix for extract calc # initialize mixer if included if config.has_drain_mixer: self.drain_mix.steam.fix() self.drain_mix.drain.fix() self.drain_mix.outlet.unfix() self.drain_mix.initialize(*args, **kwargs) _set_port(self.condense.inlet_1, self.drain_mix.outlet) if config.has_desuperheat: self.drain_mix.steam.unfix() else: self.drain_mix.steam.flow_mol.unfix() # Initialize condense section if config.has_drain_cooling: _set_port(self.condense.inlet_2, self.cooling.inlet_2) self.cooling.inlet_2.fix() else: self.condense.inlet_2.fix() if not config.has_drain_mixer and not config.has_desuperheat: self.condense.inlet_1.fix() self.condense.inlet_1.flow_mol.unfix() tempsat = value(self.condense.shell.properties_in[0].temperature_sat) temp = value(self.condense.tube.properties_in[0].temperature) if tempsat - temp < 30: init_log.warning( "The steam sat. temperature ({}) is near the feedwater" " inlet temperature ({})".format(tempsat, temp)) self.condense.initialize(*args, **kwargs) # Initialize drain cooling if included if config.has_drain_cooling: _set_port(self.cooling.inlet_1, self.condense.outlet_1) self.cooling.initialize(*args, **kwargs) # Solve all together opt = SolverFactory(kwargs.get("solver", "ipopt")) opt.options = kwargs.get("oparg", {}) assert degrees_of_freedom(self) == 0 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: res = opt.solve(self, tee=slc.tee) init_log.info("Condensing shell inlet delta T = {}".format( value(self.condense.delta_temperature_in[0]))) init_log.info("Condensing Shell outlet delta T = {}".format( value(self.condense.delta_temperature_out[0]))) init_log.info("Steam Flow = {}".format( value(self.condense.inlet_1.flow_mol[0]))) init_log.info("Initialization Complete: {}".format( idaeslog.condition(res))) from_json(self, sd=istate, wts=sp)