from pyomo.network import Port from idaes.core import (FlowsheetBlock) import Custom_prop_2 as props # import Mod2_hda_ideal_VLE as props from idaes.unit_models import Mixer from idaes.unit_models.mixer import MixingType, MomentumMixingType from idaes.core.util.model_statistics import degrees_of_freedom m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = props.NaClParameterBlock() # m.fs.properties = props.HDAParameterBlock() m.fs.Mixer = Mixer( default={ "property_package": m.fs.properties, "inlet_list": ["feed_1", "feed_2"], "energy_mixing_type": MixingType.extensive, "momentum_mixing_type": MomentumMixingType.none }) m.fs.Mixer.feed_1.flow_mass[0].fix(0.5) m.fs.Mixer.feed_1.mass_frac[0].fix(0.1) m.fs.Mixer.feed_1.temperature[0].fix(273.15 + 50) m.fs.Mixer.feed_2.flow_mass[0].fix(0.5) m.fs.Mixer.feed_2.mass_frac[0].fix(0.035) m.fs.Mixer.feed_2.temperature[0].fix(273.15 + 25) # m.fs.Mixer.outlet.temperature[0].fix(273.15 + 40) # m.fs.Mixer.mixed_state[0].dens_mass # m.fs.Mixer.mixed_state[0].viscosity # m.fs.Mixer.mixed_state[0].dens_mass_comp m.fs.Mixer.mixed_state[0].pressure_osm
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 build_boiler(fs): # Add property packages to flowsheet library fs.prop_fluegas = FlueGasParameterBlock() # Create unit models # Boiler Economizer fs.ECON = BoilerHeatExchanger( default={ "side_1_property_package": fs.prop_water, "side_2_property_package": fs.prop_fluegas, "has_pressure_change": True, "has_holdup": False, "delta_T_method": DeltaTMethod.counterCurrent, "tube_arrangement": TubeArrangement.inLine, "side_1_water_phase": "Liq", "has_radiation": False }) # Primary Superheater fs.PrSH = BoilerHeatExchanger( default={ "side_1_property_package": fs.prop_water, "side_2_property_package": fs.prop_fluegas, "has_pressure_change": True, "has_holdup": False, "delta_T_method": DeltaTMethod.counterCurrent, "tube_arrangement": TubeArrangement.inLine, "side_1_water_phase": "Liq", "has_radiation": True }) # Finishing Superheater fs.FSH = BoilerHeatExchanger( default={ "side_1_property_package": fs.prop_water, "side_2_property_package": fs.prop_fluegas, "has_pressure_change": True, "has_holdup": False, "delta_T_method": DeltaTMethod.counterCurrent, "tube_arrangement": TubeArrangement.inLine, "side_1_water_phase": "Liq", "has_radiation": True }) # Reheater fs.RH = BoilerHeatExchanger( default={ "side_1_property_package": fs.prop_water, "side_2_property_package": fs.prop_fluegas, "has_pressure_change": True, "has_holdup": False, "delta_T_method": DeltaTMethod.counterCurrent, "tube_arrangement": TubeArrangement.inLine, "side_1_water_phase": "Liq", "has_radiation": True }) # Platen Superheater fs.PlSH = Heater(default={"property_package": fs.prop_water}) #Boiler Water Wall fs.Water_wall = Heater(default={"property_package": fs.prop_water}) #Boiler Splitter (splits FSH flue gas outlet to Reheater and PrSH) fs.Spl1 = Separator( default={ "property_package": fs.prop_fluegas, "split_basis": SplittingType.totalFlow, "energy_split_basis": EnergySplittingType.equal_temperature }) # Flue gas mixer (mixing FG from Reheater and Primary SH, inlet to ECON) fs.mix1 = Mixer( default={ "property_package": fs.prop_fluegas, "inlet_list": ['Reheat_out', 'PrSH_out'], "dynamic": False }) # Mixer for Attemperator #1 (between PrSH and PlSH) fs.ATMP1 = Mixer( default={ "property_package": fs.prop_water, "inlet_list": ['Steam', 'SprayWater'], "dynamic": False }) # Build connections (streams) # Steam Route (side 1 = tube side = steam/water side) # Boiler feed water to Economizer (to be imported in full plant model) # fs.bfw2econ = Arc(source=fs.FWH8.outlet, # destination=fs.ECON.side_1_inlet) fs.econ2ww = Arc(source=fs.ECON.side_1_outlet, destination=fs.Water_wall.inlet) fs.ww2prsh = Arc(source=fs.Water_wall.outlet, destination=fs.PrSH.side_1_inlet) fs.prsh2plsh = Arc(source=fs.PrSH.side_1_outlet, destination=fs.PlSH.inlet) fs.plsh2fsh = Arc(source=fs.PlSH.outlet, destination=fs.FSH.side_1_inlet) fs.FSHtoATMP1 = Arc(source=fs.FSH.side_1_outlet, destination=fs.ATMP1.Steam) # fs.fsh2hpturbine=Arc(source=fs.ATMP1.outlet, # destination=fs.HPTinlet) #(to be imported in full plant model) # Flue gas route ------------------------------------------------------------ # water wall connected with boiler block (to fix the heat duty) # platen SH connected with boiler block (to fix the heat duty) #Finishing superheater connected with a flowsheet level constraint fs.fg_fsh2_separator = Arc(source=fs.FSH.side_2_outlet, destination=fs.Spl1.inlet) fs.fg_fsh2rh = Arc(source=fs.Spl1.outlet_1, destination=fs.RH.side_2_inlet) fs.fg_fsh2PrSH = Arc(source=fs.Spl1.outlet_2, destination=fs.PrSH.side_2_inlet) fs.fg_rhtomix = Arc(source=fs.RH.side_2_outlet, destination=fs.mix1.Reheat_out) fs.fg_prsh2mix = Arc(source=fs.PrSH.side_2_outlet, destination=fs.mix1.PrSH_out) fs.fg_mix2econ = Arc(source=fs.mix1.outlet, destination=fs.ECON.side_2_inlet)
def main(): """ Make the flowsheet object, fix some variables, and solve the problem """ # Create a Concrete Model as the top level object m = ConcreteModel() # Add a flowsheet object to the model # time_set has points at 0 and 20 as the start and end of the domain, # and a point at t=1 to allow for a step-change at this time m.fs = FlowsheetBlock(default={"dynamic": True, "time_set": [0, 1, 20]}) # Add property packages to flowsheet library m.fs.thermo_params = thermo_props.SaponificationParameterBlock() m.fs.reaction_params = reaction_props.SaponificationReactionParameterBlock( default={"property_package": m.fs.thermo_params}) # Create unit models m.fs.mix = Mixer(default={ "dynamic": False, "property_package": m.fs.thermo_params }) m.fs.Tank1 = CSTR( default={ "property_package": m.fs.thermo_params, "reaction_package": m.fs.reaction_params, "has_holdup": True, "has_equilibrium_reactions": False, "has_heat_of_reaction": True, "has_heat_transfer": True, "has_pressure_change": False }) m.fs.Tank2 = CSTR( default={ "property_package": m.fs.thermo_params, "reaction_package": m.fs.reaction_params, "has_holdup": True, "has_equilibrium_reactions": False, "has_heat_of_reaction": True, "has_heat_transfer": True, "has_pressure_change": False }) # Add pressure-flow constraints to Tank 1 m.fs.Tank1.height = Var(m.fs.time, initialize=1.0, doc="Depth of fluid in tank [m]") m.fs.Tank1.area = Var(initialize=1.0, doc="Cross-sectional area of tank [m^2]") m.fs.Tank1.flow_coeff = Var(m.fs.time, initialize=5e-5, doc="Tank outlet flow coefficient") def geometry(b, t): return b.volume[t] == b.area * b.height[t] m.fs.Tank1.geometry = Constraint(m.fs.time, rule=geometry) def outlet_flowrate(b, t): return b.control_volume.properties_out[t].flow_vol == \ b.flow_coeff[t]*b.height[t]**0.5 m.fs.Tank1.outlet_flowrate = Constraint(m.fs.time, rule=outlet_flowrate) # Add pressure-flow constraints to tank 2 m.fs.Tank2.height = Var(m.fs.time, initialize=1.0, doc="Depth of fluid in tank [m]") m.fs.Tank2.area = Var(initialize=1.0, doc="Cross-sectional area of tank [m^2]") m.fs.Tank2.flow_coeff = Var(m.fs.time, initialize=5e-5, doc="Tank outlet flow coefficient") m.fs.Tank2.geometry = Constraint(m.fs.time, rule=geometry) m.fs.Tank2.outlet_flowrate = Constraint(m.fs.time, rule=outlet_flowrate) # Make Streams to connect units m.fs.stream1 = Arc(source=m.fs.mix.outlet, destination=m.fs.Tank1.inlet) m.fs.stream2 = Arc(source=m.fs.Tank1.outlet, destination=m.fs.Tank2.inlet) # Discretize time domain m.discretizer = TransformationFactory('dae.finite_difference') m.discretizer.apply_to(m, nfe=50, wrt=m.fs.time, scheme="BACKWARD") TransformationFactory("network.expand_arcs").apply_to(m) # Set inlet and operating conditions, and some initial conditions. m.fs.mix.inlet_1.flow_vol.fix(0.5) m.fs.mix.inlet_1.conc_mol_comp[:, "H2O"].fix(55388.0) m.fs.mix.inlet_1.conc_mol_comp[:, "NaOH"].fix(100.0) m.fs.mix.inlet_1.conc_mol_comp[:, "EthylAcetate"].fix(0.0) m.fs.mix.inlet_1.conc_mol_comp[:, "SodiumAcetate"].fix(0.0) m.fs.mix.inlet_1.conc_mol_comp[:, "Ethanol"].fix(0.0) m.fs.mix.inlet_1.temperature.fix(303.15) m.fs.mix.inlet_1.pressure.fix(101325.0) m.fs.mix.inlet_2.flow_vol.fix(0.5) m.fs.mix.inlet_2.conc_mol_comp[:, "H2O"].fix(55388.0) m.fs.mix.inlet_2.conc_mol_comp[:, "NaOH"].fix(0.0) m.fs.mix.inlet_2.conc_mol_comp[:, "EthylAcetate"].fix(100.0) m.fs.mix.inlet_2.conc_mol_comp[:, "SodiumAcetate"].fix(0.0) m.fs.mix.inlet_2.conc_mol_comp[:, "Ethanol"].fix(0.0) m.fs.mix.inlet_2.temperature.fix(303.15) m.fs.mix.inlet_2.pressure.fix(101325.0) m.fs.Tank1.area.fix(1.0) m.fs.Tank1.flow_coeff.fix(0.5) m.fs.Tank1.heat_duty.fix(0.0) m.fs.Tank2.area.fix(1.0) m.fs.Tank2.flow_coeff.fix(0.5) m.fs.Tank2.heat_duty.fix(0.0) # Set initial conditions - accumulation = 0 at time = 0 m.fs.fix_initial_conditions(state="steady-state") # Initialize Units m.fs.mix.initialize() m.fs.Tank1.initialize( state_args={ "flow_vol": 1.0, "conc_mol_comp": { "H2O": 55388.0, "NaOH": 100.0, "EthylAcetate": 100.0, "SodiumAcetate": 0.0, "Ethanol": 0.0 }, "temperature": 303.15, "pressure": 101325.0 }) m.fs.Tank2.initialize( state_args={ "flow_vol": 1.0, "conc_mol_comp": { "H2O": 55388.0, "NaOH": 100.0, "EthylAcetate": 100.0, "SodiumAcetate": 0.0, "Ethanol": 0.0 }, "temperature": 303.15, "pressure": 101325.0 }) # Create a solver solver = SolverFactory('ipopt') results = solver.solve(m.fs) # Make a step disturbance in feed and solve again for t in m.fs.time: if t >= 1.0: m.fs.mix.inlet_2.conc_mol_comp[t, "EthylAcetate"].fix(90.0) results = solver.solve(m.fs) # Print results print(results) # For testing purposes return (m, results)
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)
class TurbineMultistageData(UnitModelBlockData): CONFIG = ConfigBlock() _define_turbine_multistage_config(CONFIG) def build(self): super(TurbineMultistageData, self).build() config = self.config unit_cfg = { # general unit model config "dynamic":config.dynamic, "has_holdup":config.has_holdup, "has_phase_equilibrium":config.has_phase_equilibrium, "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 = RangeSet(ni) # Adding unit models #------------------------ # Splitter to inlet that splits main flow into parallel flows for # paritial arc admission to the turbine self.inlet_split = Separator(default=self._split_cfg(unit_cfg, ni)) self.throttle_valve = SteamValve(inlet_idx, default=unit_cfg) self.inlet_stage = TurbineInletStage(inlet_idx, default=unit_cfg) # mixer to combine the parallel flows back together self.inlet_mix = Mixer(default=self._mix_cfg(unit_cfg, ni)) # add turbine sections. # inlet stage -> hp stages -> ip stages -> lp stages -> outlet stage self.hp_stages = TurbineStage(RangeSet(config.num_hp), default=unit_cfg) self.ip_stages = TurbineStage(RangeSet(config.num_ip), default=unit_cfg) self.lp_stages = TurbineStage(RangeSet(config.num_lp), default=unit_cfg) self.outlet_stage = TurbineOutletStage(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 = Separator(config.hp_split_locations, default=s_sfg_default, initialize=hp_splt_cfg) if config.ip_split_locations: self.ip_split = Separator(config.ip_split_locations, default=s_sfg_default, initialize=ip_splt_cfg) if config.lp_split_locations: self.lp_split = Separator(config.lp_split_locations, default=s_sfg_default, initialize=lp_splt_cfg) # 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.split_to_valve_stream = Arc(inlet_idx, rule=_split_to_rule) self.valve_to_inlet_stage_stream = Arc(inlet_idx, rule=_valve_to_rule) self.inlet_stage_to_mix = 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: return { "source": turbines[i].outlet, "destination": splitters[i].inlet } elif j == 2: return { "source": splitters[i].outlet_1, "destination": turbines[i + 1].inlet } else: 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 = Set(initialize=self.hp_stages.index_set() * [1, 2]) self.ip_stream_idx = Set(initialize=self.ip_stages.index_set() * [1, 2]) self.lp_stream_idx = Set(initialize=self.lp_stages.index_set() * [1, 2]) # Throw out unneeded streams _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: 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.Expression(self.flowsheet().config.time) def power(b, t): return (sum(b.inlet_stage[i].power_shaft[t] for i in b.inlet_stage) + b.outlet_stage.power_shaft[t] + sum(b.hp_stages[i].power_shaft[t] for i in b.hp_stages) + sum(b.ip_stages[i].power_shaft[t] for i in b.ip_stages) + sum(b.lp_stages[i].power_shaft[t] for i in b.lp_stages)) # Connect inlet stage to hp section # not allowing disconnection of inlet and first regular hp stage 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) 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 s_cfg = copy.copy(unit_cfg) # splitter config based on unit_cfg s_cfg.update( split_basis=SplittingType.totalFlow, ideal_separation=False, num_outlets=no, energy_split_basis=EnergySplittingType.equal_molar_enthalpy) del s_cfg["has_phase_equilibrium"] return s_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 """ m_cfg = copy.copy(unit_cfg) # splitter config based on unit_cfg m_cfg.update( num_inlets=ni, momentum_mixing_type=MomentumMixingType.minimize_and_equality) del m_cfg["has_phase_equilibrium"] return m_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 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=0, solver='ipopt', optarg={ 'tol': 1e-6, 'max_iter': 35 }): """ Initialize """ stee = True if outlvl >= 3 else False # sp is what to save to make sure state after init is same as the start # saves value, fixed, and active state, doesn't load originally free # values, this makes sure original problem spec is same but initializes # the values of free vars sp = StoreSpec.value_isfixed_isactive(only_fixed=True) istate = to_json(self, return_dict=True, wts=sp) ni = self.config.num_parallel_inlet_stages # Initialize Splitter # Fix n - 1 split fractions self.inlet_split.split_fraction[0, "outlet_1"].value = 1.0 / ni for i in self.inlet_stage_idx: if i == 1: #fix rest of splits at leaving first one free continue self.inlet_split.split_fraction[0, "outlet_{}".format(i)].fix(1.0 / ni) # fix inlet and free outlet self.inlet_split.inlet.fix() for i in self.inlet_stage_idx: ol = getattr(self.inlet_split, "outlet_{}".format(i)) ol.unfix() self.inlet_split.initialize(outlvl=outlvl, solver=solver, optarg=optarg) # free split fractions for i in self.inlet_stage_idx: self.inlet_split.split_fraction[0, "outlet_{}".format(i)].unfix() # Initialize valves for i in self.inlet_stage_idx: _set_port(self.throttle_valve[i].inlet, getattr(self.inlet_split, "outlet_{}".format(i))) self.throttle_valve[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg) # Initialize turbine for i in self.inlet_stage_idx: _set_port(self.inlet_stage[i].inlet, self.throttle_valve[i].outlet) self.inlet_stage[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg) # Initialize Mixer self.inlet_mix.use_minimum_inlet_pressure_constraint() for i in self.inlet_stage_idx: _set_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() def init_section(stages, splits, disconnects, prev_port): if 0 in splits: _set_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: _set_port(stages[i].inlet, prev_port) stages[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg) prev_port = stages[i].outlet if i in splits: _set_port(splits[i].inlet, prev_port) splits[i].initialize(outlvl=outlvl, solver=solver, optarg=optarg) prev_port = splits[i].outlet_1 return prev_port prev_port = self.inlet_mix.outlet prev_port = init_section(self.hp_stages, self.hp_split, self.config.hp_disconnect, prev_port) if len(self.hp_stages) in self.config.hp_disconnect: prev_port = self.ip_stages[1].inlet prev_port = init_section(self.ip_stages, self.ip_split, self.config.ip_disconnect, prev_port) if len(self.ip_stages) in self.config.ip_disconnect: prev_port = self.lp_stages[1].inlet prev_port = init_section(self.lp_stages, self.lp_split, self.config.lp_disconnect, prev_port) _set_port(self.outlet_stage.inlet, prev_port) self.outlet_stage.initialize(outlvl=outlvl, solver=solver, optarg=optarg) from_json(self, sd=istate, wts=sp)
def build(self): super(TurbineMultistageData, self).build() config = self.config unit_cfg = { # general unit model config "dynamic":config.dynamic, "has_holdup":config.has_holdup, "has_phase_equilibrium":config.has_phase_equilibrium, "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 = RangeSet(ni) # Adding unit models #------------------------ # Splitter to inlet that splits main flow into parallel flows for # paritial arc admission to the turbine self.inlet_split = Separator(default=self._split_cfg(unit_cfg, ni)) self.throttle_valve = SteamValve(inlet_idx, default=unit_cfg) self.inlet_stage = TurbineInletStage(inlet_idx, default=unit_cfg) # mixer to combine the parallel flows back together self.inlet_mix = Mixer(default=self._mix_cfg(unit_cfg, ni)) # add turbine sections. # inlet stage -> hp stages -> ip stages -> lp stages -> outlet stage self.hp_stages = TurbineStage(RangeSet(config.num_hp), default=unit_cfg) self.ip_stages = TurbineStage(RangeSet(config.num_ip), default=unit_cfg) self.lp_stages = TurbineStage(RangeSet(config.num_lp), default=unit_cfg) self.outlet_stage = TurbineOutletStage(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 = Separator(config.hp_split_locations, default=s_sfg_default, initialize=hp_splt_cfg) if config.ip_split_locations: self.ip_split = Separator(config.ip_split_locations, default=s_sfg_default, initialize=ip_splt_cfg) if config.lp_split_locations: self.lp_split = Separator(config.lp_split_locations, default=s_sfg_default, initialize=lp_splt_cfg) # 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.split_to_valve_stream = Arc(inlet_idx, rule=_split_to_rule) self.valve_to_inlet_stage_stream = Arc(inlet_idx, rule=_valve_to_rule) self.inlet_stage_to_mix = 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: return { "source": turbines[i].outlet, "destination": splitters[i].inlet } elif j == 2: return { "source": splitters[i].outlet_1, "destination": turbines[i + 1].inlet } else: 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 = Set(initialize=self.hp_stages.index_set() * [1, 2]) self.ip_stream_idx = Set(initialize=self.ip_stages.index_set() * [1, 2]) self.lp_stream_idx = Set(initialize=self.lp_stages.index_set() * [1, 2]) # Throw out unneeded streams _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: 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.Expression(self.flowsheet().config.time) def power(b, t): return (sum(b.inlet_stage[i].power_shaft[t] for i in b.inlet_stage) + b.outlet_stage.power_shaft[t] + sum(b.hp_stages[i].power_shaft[t] for i in b.hp_stages) + sum(b.ip_stages[i].power_shaft[t] for i in b.ip_stages) + sum(b.lp_stages[i].power_shaft[t] for i in b.lp_stages)) # Connect inlet stage to hp section # not allowing disconnection of inlet and first regular hp stage 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) TransformationFactory("network.expand_arcs").apply_to(self)
def test_serialize_flowsheet(): # Construct the model from idaes/examples/workshops/Module_2_Flowsheet/Module_2_Flowsheet_Solution.ipynb m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.thermo_params = thermo_props.HDAParameterBlock() m.fs.reaction_params = reaction_props.HDAReactionParameterBlock( default={"property_package": m.fs.thermo_params}) m.fs.M101 = Mixer( default={ "property_package": m.fs.thermo_params, "inlet_list": ["toluene_feed", "hydrogen_feed", "vapor_recycle"] }) m.fs.H101 = Heater( default={ "property_package": m.fs.thermo_params, "has_pressure_change": False, "has_phase_equilibrium": True }) m.fs.R101 = StoichiometricReactor( default={ "property_package": m.fs.thermo_params, "reaction_package": m.fs.reaction_params, "has_heat_of_reaction": True, "has_heat_transfer": True, "has_pressure_change": False }) m.fs.F101 = Flash( default={ "property_package": m.fs.thermo_params, "has_heat_transfer": True, "has_pressure_change": True }) m.fs.S101 = Splitter( default={ "property_package": m.fs.thermo_params, "ideal_separation": False, "outlet_list": ["purge", "recycle"] }) m.fs.C101 = PressureChanger( default={ "property_package": m.fs.thermo_params, "compressor": True, "thermodynamic_assumption": ThermodynamicAssumption.isothermal }) m.fs.F102 = Flash( default={ "property_package": m.fs.thermo_params, "has_heat_transfer": True, "has_pressure_change": True }) m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet) m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet) m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.F101.inlet) m.fs.s06 = Arc(source=m.fs.F101.vap_outlet, destination=m.fs.S101.inlet) m.fs.s08 = Arc(source=m.fs.S101.recycle, destination=m.fs.C101.inlet) m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.vapor_recycle) m.fs.s10 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet) fss = FlowsheetSerializer() fss.serialize_flowsheet(m.fs) unit_models = fss.get_unit_models() unit_model_names_types = [] for unit_model in unit_models: unit_model_names_types.append(unit_models[unit_model]) unit_models_names_type_truth = [{ 'name': 'M101', 'type': 'mixer' }, { 'name': 'H101', 'type': 'heater' }, { 'name': 'R101', 'type': 'stoichiometric_reactor' }, { 'name': 'F101', 'type': 'flash' }, { 'name': 'S101', 'type': 'separator' }, { 'name': 'C101', 'type': 'pressure_changer' }, { 'name': 'F102', 'type': 'flash' }] set_result = set(tuple(sorted(d.items())) for d in unit_model_names_types) set_truth = set( tuple(sorted(d.items())) for d in unit_models_names_type_truth) difference = list(set_truth.symmetric_difference(set_result)) assert len(difference) == 0 # TODO Figure out how to test ports. Maybe find out if we can find the parent component for the port? # ports = fss.get_ports() # assert ports == {"<pyomo.network.port.SimplePort object at 0x7fe8d0d79278>": "<idaes.core.process_block._ScalarMixer object at 0x7fe8d0d60360>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0d792e8>": "<idaes.core.process_block._ScalarMixer object at 0x7fe8d0d60360>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0d79358>": "<idaes.core.process_block._ScalarMixer object at 0x7fe8d0d60360>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0d793c8>": "<idaes.core.process_block._ScalarMixer object at 0x7fe8d0d60360>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0d797b8>": "<idaes.core.process_block._ScalarHeater object at 0x7fe8d0db74c8>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0d79828>": "<idaes.core.process_block._ScalarHeater object at 0x7fe8d0db74c8>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0d79a58>": "<idaes.core.process_block._ScalarStoichiometricReactor object at 0x7fe8d0de2ab0>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0d79ac8>": "<idaes.core.process_block._ScalarStoichiometricReactor object at 0x7fe8d0de2ab0>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0d79eb8>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8d0e0fdc8>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41128>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8d0e0fdc8>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41198>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8d0e0fdc8>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0d79f98>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8d0e0fdc8>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41048>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8d0e0fdc8>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e410b8>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8d0e0fdc8>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41278>": "<idaes.core.process_block._ScalarSeparator object at 0x7fe8d0e45708>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41588>": "<idaes.core.process_block._ScalarSeparator object at 0x7fe8d0e45708>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e415f8>": "<idaes.core.process_block._ScalarSeparator object at 0x7fe8d0e45708>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41828>": "<idaes.core.process_block._ScalarPressureChanger object at 0x7fe8d0e686c0>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41898>": "<idaes.core.process_block._ScalarPressureChanger object at 0x7fe8d0e686c0>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41c88>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8e1405cf0>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41eb8>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8e1405cf0>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41f28>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8e1405cf0>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41e48>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8e1405cf0>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41dd8>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8e1405cf0>", # "<pyomo.network.port.SimplePort object at 0x7fe8d0e41d68>": "<idaes.core.process_block._ScalarFlash object at 0x7fe8e1405cf0>" # } named_edges_results = {} edges = fss.get_edges() for edge in edges: named_edges_results[edge.getname()] = [ x.getname() for x in edges[edge] ] named_edges_truth = { 'M101': ['H101'], 'H101': ['R101'], 'R101': ['F101'], 'F101': ['S101', 'F102'], 'S101': ['C101'], 'C101': ['M101'] } assert named_edges_results == named_edges_truth
def main(): # Create a Concrete Model as the top level object m = ConcreteModel() # Add a flowsheet object to the model m.fs = FlowsheetBlock(default={"dynamic": False}) # Add property packages to flowsheet library m.fs.thermo_params = thermo_props.HDAParameterBlock() m.fs.reaction_params = reaction_props.HDAReactionParameterBlock( default={"property_package": m.fs.thermo_params}) # Create unit models m.fs.M101 = Mixer( default={ "property_package": m.fs.thermo_params, "inlet_list": ["toluene_feed", "hydrogen_feed", "vapor_recycle"] }) m.fs.H101 = Heater( default={ "property_package": m.fs.thermo_params, "has_pressure_change": False, "has_phase_equilibrium": True }) m.fs.R101 = StoichiometricReactor( default={ "property_package": m.fs.thermo_params, "reaction_package": m.fs.reaction_params, "has_heat_of_reaction": True, "has_heat_transfer": True, "has_pressure_change": False }) m.fs.F101 = Flash( default={ "property_package": m.fs.thermo_params, "has_heat_transfer": True, "has_pressure_change": True }) m.fs.S101 = Splitter( default={ "property_package": m.fs.thermo_params, "ideal_separation": False, "outlet_list": ["purge", "recycle"] }) # This is needed to avoid pressure degeneracy in recylce loop m.fs.C101 = PressureChanger( default={ "property_package": m.fs.thermo_params, "compressor": True, "thermodynamic_assumption": ThermodynamicAssumption.isothermal }) m.fs.F102 = Flash( default={ "property_package": m.fs.thermo_params, "has_heat_transfer": True, "has_pressure_change": True }) m.fs.H101.control_volume.scaling_factor_energy = 1e-3 m.fs.R101.control_volume.scaling_factor_energy = 1e-3 m.fs.F101.control_volume.scaling_factor_energy = 1e-3 m.fs.C101.control_volume.scaling_factor_energy = 1e-3 m.fs.F102.control_volume.scaling_factor_energy = 1e-3 # Connect units m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet) m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet) m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.F101.inlet) m.fs.s06 = Arc(source=m.fs.F101.vap_outlet, destination=m.fs.S101.inlet) m.fs.s08 = Arc(source=m.fs.S101.recycle, destination=m.fs.C101.inlet) m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.vapor_recycle) m.fs.s10 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet) TransformationFactory("network.expand_arcs").apply_to(m) # Set operating conditions m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Vap", "benzene"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Vap", "toluene"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Vap", "hydrogen"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Vap", "methane"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Liq", "benzene"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Liq", "toluene"].fix(0.30) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Liq", "hydrogen"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Liq", "methane"].fix(1e-5) m.fs.M101.toluene_feed.temperature.fix(303.2) m.fs.M101.toluene_feed.pressure.fix(350000) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Vap", "benzene"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Vap", "toluene"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Vap", "hydrogen"].fix(0.30) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Vap", "methane"].fix(0.02) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Liq", "benzene"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Liq", "toluene"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Liq", "hydrogen"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Liq", "methane"].fix(1e-5) m.fs.M101.hydrogen_feed.temperature.fix(303.2) m.fs.M101.hydrogen_feed.pressure.fix(350000) m.fs.H101.outlet.temperature.fix(600) m.fs.R101.conversion = Var(initialize=0.75, bounds=(0, 1)) m.fs.R101.conv_constraint = Constraint( expr=m.fs.R101.conversion * m.fs.R101.inlet.flow_mol_phase_comp[0, "Vap", "toluene"] == ( m.fs.R101.inlet.flow_mol_phase_comp[0, "Vap", "toluene"] - m.fs.R101.outlet.flow_mol_phase_comp[0, "Vap", "toluene"])) m.fs.R101.conversion.fix(0.75) m.fs.R101.heat_duty.fix(0) m.fs.F101.vap_outlet.temperature.fix(325.0) m.fs.F101.deltaP.fix(0) m.fs.S101.split_fraction[0, "purge"].fix(0.2) m.fs.C101.outlet.pressure.fix(350000) m.fs.F102.vap_outlet.temperature.fix(375) m.fs.F102.deltaP.fix(-200000) # Define expressions # Product purity m.fs.purity = Expression( expr=m.fs.F102.vap_outlet.flow_mol_phase_comp[0, "Vap", "benzene"] / (m.fs.F102.vap_outlet.flow_mol_phase_comp[0, "Vap", "benzene"] + m.fs.F102.vap_outlet.flow_mol_phase_comp[0, "Vap", "toluene"])) # Operating cost ($/yr) m.fs.cooling_cost = Expression(expr=0.212e-7 * -m.fs.F101.heat_duty[0] + 0.212e-7 * -m.fs.R101.heat_duty[0]) m.fs.heating_cost = Expression(expr=2.2e-7 * m.fs.H101.heat_duty[0] + 1.9e-7 * m.fs.F102.heat_duty[0]) m.fs.operating_cost = Expression( expr=(3600 * 24 * 365 * (m.fs.heating_cost + m.fs.cooling_cost))) print(degrees_of_freedom(m)) # Initialize Units # Define method for initialising each block def function(unit): unit.initialize(outlvl=1) # Create instance of sequential decomposition tool seq = SequentialDecomposition() seq.options.select_tear_method = "heuristic" seq.options.tear_method = "Wegstein" seq.options.iterLim = 5 # Determine tear stream and calculation order G = seq.create_graph(m) heu_result = seq.tear_set_arcs(G, method="heuristic") order = seq.calculation_order(G) # Display tear stream and calculation order for o in heu_result: print(o.name) for o in order: for oo in o: print(oo.name) # Set guesses for tear stream tear_guesses = { "flow_mol_phase_comp": { (0, "Vap", "benzene"): 1e-5, (0, "Vap", "toluene"): 1e-5, (0, "Vap", "hydrogen"): 0.30, (0, "Vap", "methane"): 0.02, (0, "Liq", "benzene"): 1e-5, (0, "Liq", "toluene"): 0.30, (0, "Liq", "hydrogen"): 1e-5, (0, "Liq", "methane"): 1e-5 }, "temperature": { 0: 303 }, "pressure": { 0: 350000 } } seq.set_guesses_for(m.fs.H101.inlet, tear_guesses) # Run sequential initialization seq.run(m, function) # # Create a solver solver = SolverFactory('ipopt') solver.options = {'tol': 1e-6} solver.options = {'tol': 1e-6, 'max_iter': 5000} results = solver.solve(m, tee=True) # Print results print("M101 Outlet") m.fs.M101.outlet.display() print("H101 Outlet") m.fs.H101.outlet.display() print("R101 Outlet") m.fs.R101.outlet.display() print("F101") m.fs.F101.liq_outlet.display() m.fs.F101.vap_outlet.display() print("F102") m.fs.F102.liq_outlet.display() m.fs.F102.vap_outlet.display() print("Purge") m.fs.S101.purge.display() print("Purity:", value(m.fs.purity)) # Optimize process m.fs.objective = Objective(sense=minimize, expr=m.fs.operating_cost) # Decision variables m.fs.H101.outlet.temperature.unfix() m.fs.R101.heat_duty.unfix() m.fs.F101.vap_outlet.temperature.unfix() m.fs.F102.vap_outlet.temperature.unfix() m.fs.F102.deltaP.unfix() # Variable bounds m.fs.H101.outlet.temperature[0].setlb(500) m.fs.H101.outlet.temperature[0].setub(600) m.fs.R101.outlet.temperature[0].setlb(600) m.fs.R101.outlet.temperature[0].setub(800) m.fs.F101.vap_outlet.temperature[0].setlb(298.0) m.fs.F101.vap_outlet.temperature[0].setub(450.0) m.fs.F102.vap_outlet.temperature[0].setlb(298.0) m.fs.F102.vap_outlet.temperature[0].setub(450.0) m.fs.F102.vap_outlet.pressure[0].setlb(105000) m.fs.F102.vap_outlet.pressure[0].setub(110000) # Additional Constraints m.fs.overhead_loss = Constraint( expr=m.fs.F101.vap_outlet.flow_mol_phase_comp[0, "Vap", "benzene"] <= 0.20 * m.fs.R101.outlet.flow_mol_phase_comp[0, "Vap", "benzene"]) m.fs.product_flow = Constraint( expr=m.fs.F102.vap_outlet.flow_mol_phase_comp[0, "Vap", "benzene"] >= 0.15) m.fs.product_purity = Constraint(expr=m.fs.purity >= 0.80) # Create a solver solver = SolverFactory('ipopt') solver.options = {'tol': 1e-6} solver.options = {'tol': 1e-6, 'max_iter': 5000} results = solver.solve(m, tee=True) # Print optimization results print() print("Optimal Solution") m.fs.operating_cost.display() m.fs.H101.heat_duty.display() m.fs.R101.heat_duty.display() m.fs.F101.heat_duty.display() m.fs.F102.heat_duty.display() # Print results print("M101 Outlet") m.fs.M101.outlet.display() print("H101 Outlet") m.fs.H101.outlet.display() print("R101 Outlet") m.fs.R101.outlet.display() print("F101") m.fs.F101.liq_outlet.display() m.fs.F101.vap_outlet.display() print("F102") m.fs.F102.liq_outlet.display() m.fs.F102.vap_outlet.display() print("Purge") m.fs.S101.purge.display() print("Recycle") m.fs.S101.recycle.display() print("Purity:", value(m.fs.purity)) # For testing purposes return (m, results)
def create_model(): m = pe.ConcreteModel() m.fs = fs = FlowsheetBlock(default={"dynamic": False}) fs.vapor_props = vapor_props = PhysicalParameterBlock( default={"valid_phase": 'Vap'}) fs.properties = props = PhysicalParameterBlock( default={"valid_phase": ('Vap', 'Liq')}) fs.reaction_params = MethanolReactionParameterBlock( default={'property_package': vapor_props}) fs.feed = feed = Feed(default={"property_package": vapor_props}) fs.compressor1 = IdealGasIsentropicCompressor( default={ "property_package": vapor_props, "has_phase_equilibrium": False }) fs.cooler1 = Heater(default={ "property_package": vapor_props, "has_phase_equilibrium": False }) fs.compressor2 = IdealGasIsentropicCompressor( default={ "property_package": vapor_props, "has_phase_equilibrium": False }) fs.equal_electric = pe.Constraint( expr=fs.compressor1.work[0.0] == fs.compressor2.work[0.0]) fs.mixer = Mixer( default={ 'property_package': vapor_props, 'inlet_list': ['feed', 'recycle'], 'momentum_mixing_type': MomentumMixingType.equality }) # Reactor fs.reactor = StoichiometricReactor( default={ 'property_package': vapor_props, 'reaction_package': fs.reaction_params, 'has_heat_of_reaction': True, 'has_pressure_change': False }) fs.reactor.conversion_eq = pe.Var() fs.reactor.t_inv = pe.Var() fs.reactor.p_sq_inv = pe.Var() fs.reactor.conversion = pe.Var() fs.reactor.consumption_rate = pe.Var() fs.reactor.t_inv_con = pe.Constraint(expr=fs.reactor.t_inv * fs.reactor.outlet.temperature[0] == 1) fs.reactor.p_sq_inv_con = pe.Constraint( expr=fs.reactor.p_sq_inv * fs.reactor.inlet.pressure[0]**2 == 1) fs.reactor.conversion_eq_con = pe.Constraint(expr=( fs.reactor.conversion_eq == 0.415 * (1 - 26.25 * pe.exp(-18 * fs.reactor.t_inv) * fs.reactor.p_sq_inv))) fs.reactor.conversion_con = pe.Constraint( expr=(fs.reactor.conversion == fs.reactor.conversion_eq * (1 - pe.exp(-5)) * (fs.reactor.inlet.mole_frac[0, "H2"] + fs.reactor.inlet.mole_frac[0, "CO"] + fs.reactor.inlet.mole_frac[0, "CH3OH"]))) fs.reactor.consumption_rate_con = pe.Constraint( expr=(fs.reactor.consumption_rate == fs.reactor.conversion * fs.reactor.inlet.mole_frac[0, "H2"] * fs.reactor.inlet.flow_mol[0])) fs.reactor.h2_consumption_con = pe.Constraint(expr=( fs.reactor.outlet.flow_mol[0] * fs.reactor.outlet.mole_frac[0, "H2"] == fs.reactor.inlet.flow_mol[0] * fs.reactor.inlet.mole_frac[0, "H2"] - fs.reactor.consumption_rate)) fs.expander = Expander(default={ 'property_package': vapor_props, 'has_phase_equilibrium': False }) fs.cooler2 = Heater(default={ "property_package": vapor_props, "has_phase_equilibrium": False }) fs.flash = Flash(default={"property_package": props}) fs.purge_splitter = Separator( default={ 'property_package': vapor_props, 'outlet_list': ['purge', 'recycle'], 'ideal_separation': False }) fs.compressor3 = IdealGasIsentropicCompressor( default={ "property_package": vapor_props, "has_phase_equilibrium": False }) ########################### # Set scaling factors ########################### fs.compressor1.control_volume.scaling_factor_energy.value = 1 fs.compressor2.control_volume.scaling_factor_energy.value = 1 fs.cooler1.control_volume.scaling_factor_energy.value = 1 fs.flash.control_volume.scaling_factor_energy.value = 1 fs.reactor.control_volume.scaling_factor_energy.value = 1 fs.cooler2.control_volume.scaling_factor_energy.value = 1 fs.compressor3.control_volume.scaling_factor_energy.value = 1 fs.mixer.scaling_factor_energy.value = 1 fs.cooler1.control_volume.scaling_factor_pressure.value = 1 fs.flash.control_volume.scaling_factor_pressure.value = 1 fs.reactor.control_volume.scaling_factor_pressure.value = 1 fs.cooler2.control_volume.scaling_factor_pressure.value = 1 ########################### # Objective ########################### m.objective = pe.Objective(expr=(-fs.flash.liq_outlet.flow_mol[0.0])) ########################### # Connect Units ########################### fs.stream1 = network.Arc(source=feed.outlet, destination=fs.compressor1.inlet) fs.stream2 = network.Arc(source=fs.compressor1.outlet, destination=fs.cooler1.inlet) fs.stream3 = network.Arc(source=fs.cooler1.outlet, destination=fs.compressor2.inlet) fs.stream4 = network.Arc(source=fs.compressor2.outlet, destination=fs.mixer.feed) fs.stream5 = network.Arc(source=fs.mixer.outlet, destination=fs.reactor.inlet) fs.stream6 = network.Arc(source=fs.reactor.outlet, destination=fs.expander.inlet) fs.stream7 = network.Arc(source=fs.expander.outlet, destination=fs.cooler2.inlet) fs.stream8 = network.Arc(source=fs.cooler2.outlet, destination=fs.flash.inlet) fs.stream9 = network.Arc(source=fs.flash.vap_outlet, destination=fs.purge_splitter.inlet) fs.stream10 = network.Arc(source=fs.purge_splitter.recycle, destination=fs.compressor3.inlet) fs.stream11 = network.Arc(source=fs.compressor3.outlet, destination=fs.mixer.recycle) pe.TransformationFactory("network.expand_arcs").apply_to(m) ########################### # Set problem specs ########################### feed.flow_mol.fix(3.40898) feed.pressure.fix(1) feed.temperature.fix(3) feed.mole_frac[0.0, "CH4"].fix(0.05) feed.mole_frac[0.0, "CO"].fix(0.3) feed.mole_frac[0.0, "H2"].fix(0.6) feed.mole_frac[0.0, "CH3OH"].fix(0.05) fs.cooler1.heat_duty[0.0].setub(0) # it is a cooler fs.cooler1.outlet.temperature[0.0].setlb(3) fs.flash.heat_duty.fix(0) fs.flash.deltaP.fix(0) fs.cooler2.heat_duty[0.0].setub(0) # it is a cooler fs.cooler2.outlet.temperature[0.0].setlb(3) fs.compressor2.outlet.pressure[0.0].setub(10) fs.flash.liq_outlet.mole_frac[0.0, 'CH3OH'].expr.setlb(0.9) fs.purge_splitter.split_fraction[0.0, 'purge'].fix(0.05) ########################### # Prepare for initialization ########################### fs.compressor1.outlet.pressure.fix(5) fs.compressor2.outlet.pressure.fix(10) fs.cooler1.outlet.temperature.fix(3) fs.expander.outlet.pressure[0.0].fix(5) fs.cooler2.outlet.temperature[0.0].fix(3) fs.flash.liq_outlet.mole_frac[0.0, 'CH3OH'].expr.setlb(None) # Setup decomposition process seq = network.SequentialDecomposition() seq.options.select_tear_method = "heuristic" seq.options.tear_method = "Wegstein" seq.options.iterLim = 5 # Determine tear stream and calculation order G = seq.create_graph(m) heu_result = seq.tear_set_arcs(G, method="heuristic") order = seq.calculation_order(G) # Display tear stream and calculation order print("Tear") for o in heu_result: print(o.name) print("Order") for o in order: for oo in o: print(oo.name) # Set guesses for tear stream tear_guesses = { "flow_mol": { 0: 10 }, "mole_frac": { (0, 'CH3OH'): 0.06, (0, 'CH4'): 0.21, (0, 'CO'): 0.24, (0, 'H2'): 0.50 }, "temperature": { 0: 4.4 }, "pressure": { 0: 15 } } seq.set_guesses_for(m.fs.reactor.inlet, tear_guesses) # Define method for initialising each block def function(unit): unit.initialize(outlvl=1) # Run sequential initialisation seq.run(m, function) ########################### # Unfix vars that were fixed for initialization ########################### m.fs.compressor1.outlet.pressure.unfix() m.fs.compressor2.outlet.pressure.unfix() m.fs.cooler1.outlet.temperature.unfix() m.fs.expander.outlet.pressure.unfix() m.fs.cooler2.outlet.temperature.unfix() m.fs.flash.liq_outlet.mole_frac[0.0, 'CH3OH'].expr.setlb(0.9) return m
from pyomo.environ import ConcreteModel, SolverFactory, Constraint, value from idaes.core import FlowsheetBlock import Mod2_hda_ideal_VLE as thermo_props from idaes.unit_models import Mixer from idaes.core.util.model_statistics import degrees_of_freedom m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = thermo_props.HDAParameterBlock() m.fs.Mixer = Mixer(default={ "property_package": m.fs.properties, "inlet_list": ["feed_1", "feed_2"] }) # for comp in m.fs.properties.component_list: for phase in m.fs.properties.phase_list: m.fs.Mixer.feed_1.flow_mol_phase_comp[0, phase, comp].fix(1e-5) m.fs.Mixer.feed_2.flow_mol_phase_comp[0, phase, comp].fix(1e-5) print("Degrees of Freedom =", degrees_of_freedom(m)) # m.fs.Mixer.report()
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