def build_ideal_naocl_mixer_unit(model): model.fs.ideal_naocl_mixer_unit = Mixer( default={ "property_package": model.fs.ideal_naocl_thermo_params, "inlet_list": ["inlet_stream", "naocl_stream"], }) # add new constraint for dosing rate (deactivate constraint for OCl_-) dr = value( model.fs.ideal_naocl_mixer_unit.naocl_stream.flow_mol[0] * model.fs.ideal_naocl_mixer_unit.naocl_stream.mole_frac_comp[0, "OCl_-"] * model.fs.ideal_naocl_thermo_params.get_component("OCl_-").mw) model.fs.ideal_naocl_mixer_unit.dosing_rate = Var( initialize=dr, domain=NonNegativeReals, units=pyunits.kg / pyunits.s, ) def _dosing_rate_cons(blk): return blk.dosing_rate == ( blk.naocl_stream.flow_mol[0] * blk.naocl_stream.mole_frac_comp[0, "OCl_-"] * blk.naocl_stream_state[0].params.get_component("OCl_-").mw) + ( blk.naocl_stream.flow_mol[0] * blk.naocl_stream.mole_frac_comp[0, "Na_+"] * blk.naocl_stream_state[0].params.get_component("Na_+").mw) model.fs.ideal_naocl_mixer_unit.dosing_cons = Constraint( rule=_dosing_rate_cons)
def demo_flowsheet(): """Semi-complicated demonstration flowsheet. """ m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.BT_props = BTXParameterBlock() m.fs.M01 = Mixer(default={"property_package": m.fs.BT_props}) m.fs.H02 = Heater(default={"property_package": m.fs.BT_props}) m.fs.F03 = Flash(default={"property_package": m.fs.BT_props}) m.fs.s01 = Arc(source=m.fs.M01.outlet, destination=m.fs.H02.inlet) m.fs.s02 = Arc(source=m.fs.H02.outlet, destination=m.fs.F03.inlet) TransformationFactory("network.expand_arcs").apply_to(m.fs) m.fs.properties = SWCO2ParameterBlock() m.fs.main_compressor = PressureChanger( default={'dynamic': False, 'property_package': m.fs.properties, 'compressor': True, 'thermodynamic_assumption': ThermodynamicAssumption.isentropic}) m.fs.bypass_compressor = PressureChanger( default={'dynamic': False, 'property_package': m.fs.properties, 'compressor': True, 'thermodynamic_assumption': ThermodynamicAssumption.isentropic}) m.fs.turbine = PressureChanger( default={'dynamic': False, 'property_package': m.fs.properties, 'compressor': False, 'thermodynamic_assumption': ThermodynamicAssumption.isentropic}) m.fs.boiler = Heater(default={'dynamic': False, 'property_package': m.fs.properties, 'has_pressure_change': True}) m.fs.FG_cooler = Heater(default={'dynamic': False, 'property_package': m.fs.properties, 'has_pressure_change': True}) m.fs.pre_boiler = Heater(default={'dynamic': False, 'property_package': m.fs.properties, 'has_pressure_change': False}) m.fs.HTR_pseudo_tube = Heater(default={'dynamic': False, 'property_package': m.fs.properties, 'has_pressure_change': True}) m.fs.LTR_pseudo_tube = Heater(default={'dynamic': False, 'property_package': m.fs.properties, 'has_pressure_change': True}) return m.fs
def build_flowsheet(): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.BT_props = BTXParameterBlock() m.fs.M01 = Mixer(default={"property_package": m.fs.BT_props}) m.fs.H02 = Heater(default={"property_package": m.fs.BT_props}) m.fs.F03 = Flash(default={"property_package": m.fs.BT_props}) m.fs.s01 = Arc(source=m.fs.M01.outlet, destination=m.fs.H02.inlet) m.fs.s02 = Arc(source=m.fs.H02.outlet, destination=m.fs.F03.inlet) TransformationFactory("network.expand_arcs").apply_to(m.fs) return m
def concrete_model_base(m): """ Concrete and declare all units and streams in the model. Optional keyword arguments: --------------------------- :param m: the model :return: return the model """ m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.thermo_params = thermo_props.MethaneParameterBlock() # Declare all Units m.fs.HX1 = Heater(default={"property_package": m.fs.thermo_params}) m.fs.HX2a = Heater(default={"property_package": m.fs.thermo_params}) m.fs.HX2b = Heater(default={"property_package": m.fs.thermo_params}) m.fs.Mix1 = Mixer(default={ "dynamic": False, "property_package": m.fs.thermo_params }) m.fs.Mix2 = Mixer(default={ "dynamic": False, "property_package": m.fs.thermo_params }) m.fs.Mix3 = Mixer(default={ "dynamic": False, "property_package": m.fs.thermo_params }) m.fs.Split1 = Separator( default={ "dynamic": False, "split_basis": SplittingType.componentFlow, "property_package": m.fs.thermo_params }) m.fs.Reformer = GibbsReactor( default={ "dynamic": False, "property_package": m.fs.thermo_params, "has_pressure_change": False, "has_heat_transfer": True }) m.fs.SOFC = GibbsReactor( default={ "dynamic": False, "property_package": m.fs.thermo_params, "has_pressure_change": False, "has_heat_transfer": True }) m.fs.Burner = GibbsReactor( default={ "dynamic": False, "property_package": m.fs.thermo_params, "has_pressure_change": False, "has_heat_transfer": True }) # Declare all Streams m.fs.stream0 = Arc(source=m.fs.Mix1.outlet, destination=m.fs.HX1.inlet) m.fs.stream1 = Arc(source=m.fs.Split1.outlet_1, destination=m.fs.HX2b.inlet) m.fs.stream2 = Arc(source=m.fs.HX1.outlet, destination=m.fs.Reformer.inlet) m.fs.stream3 = Arc(source=m.fs.Split1.outlet_2, destination=m.fs.HX2a.inlet) m.fs.stream4 = Arc(source=m.fs.Reformer.outlet, destination=m.fs.Mix2.inlet_1) m.fs.stream5 = Arc(source=m.fs.HX2b.outlet, destination=m.fs.Mix2.inlet_2) m.fs.stream6 = Arc(source=m.fs.Mix2.outlet, destination=m.fs.SOFC.inlet) m.fs.stream7 = Arc(source=m.fs.HX2a.outlet, destination=m.fs.Mix3.inlet_2) m.fs.stream8 = Arc(source=m.fs.SOFC.outlet, destination=m.fs.Mix3.inlet_1) m.fs.stream9 = Arc(source=m.fs.Mix3.outlet, destination=m.fs.Burner.inlet) TransformationFactory("network.expand_arcs").apply_to(m) return m
def build(self): """ Begin building model (pre-DAE transformation). Args: None Returns: None """ # Call UnitModel.build to setup dynamics super().build() # Build Control Volume self.control_volume = ControlVolume0DBlock(default={ "dynamic": self.config.dynamic, "has_holdup": self.config.has_holdup, "property_package": self.config.property_package, "property_package_args": self.config.property_package_args}) self.control_volume.add_geometry() self.control_volume.add_state_blocks(has_phase_equilibrium=False) self.control_volume.add_material_balances( balance_type=self.config.material_balance_type,) self.control_volume.add_energy_balances( balance_type=self.config.energy_balance_type, has_heat_transfer=self.config.has_heat_transfer) self.control_volume.add_momentum_balances( balance_type=self.config.momentum_balance_type, has_pressure_change=True) self.flash = HelmPhaseSeparator( default={ "dynamic": False, "property_package": self.config.property_package, } ) self.mixer = Mixer( default={ "dynamic": False, "property_package": self.config.property_package, "inlet_list": ["FeedWater", "SaturatedWater"], "mixed_state_block": self.control_volume.properties_in, } ) # instead of creating a new block use control volume to return solution # Inlet Ports # FeedWater to Drum (from Pipe or Economizer) self.feedwater_inlet = Port(extends=self.mixer.FeedWater) # Sat water from water wall self.water_steam_inlet = Port(extends=self.flash.inlet) # Exit Ports # Liquid to Downcomer # self.liquid_outlet = Port(extends=self.mixer.outlet) self.add_outlet_port('liquid_outlet', self.control_volume) # Steam to superheaters self.steam_outlet = Port(extends=self.flash.vap_outlet) # constraint to make pressures of two inlets of drum mixer the same @self.Constraint(self.flowsheet().config.time, doc="Mixter pressure identical") def mixer_pressure_eqn(b, t): return b.mixer.SaturatedWater.pressure[t]*1e-6 == \ b.mixer.FeedWater.pressure[t]*1e-6 self.stream_flash_out = Arc( source=self.flash.liq_outlet, destination=self.mixer.SaturatedWater ) # Pyomo arc connect flash liq_outlet with mixer SaturatedWater inlet pyo.TransformationFactory("network.expand_arcs").apply_to(self) # Add object references self.volume = pyo.Reference(self.control_volume.volume) # Set references to balance terms at unit level if (self.config.has_heat_transfer is True and self.config.energy_balance_type != EnergyBalanceType.none): self.heat_duty = pyo.Reference(self.control_volume.heat) if (self.config.has_pressure_change is True and self.config.momentum_balance_type != 'none'): self.deltaP = pyo.Reference(self.control_volume.deltaP) # Set Unit Geometry and Holdup Volume self._set_geometry() # Construct performance equations self._make_performance()
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)
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": "Vap", "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": "Vap", "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": "Vap", "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)
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)
def make_model(horizon=6, ntfe=60, ntcp=2, inlet_E=11.91, inlet_S=12.92, steady=False, bounds=False): time_set = [0, horizon] m = ConcreteModel(name='CSTR model for testing') if steady: m.fs = FlowsheetBlock(default={'dynamic': False}) else: m.fs = FlowsheetBlock(default={'dynamic': True, 'time_set': time_set}) m.fs.properties = AqueousEnzymeParameterBlock() m.fs.reactions = EnzymeReactionParameterBlock( default={'property_package': m.fs.properties}) m.fs.cstr = CSTR( default={ 'has_holdup': True, 'property_package': m.fs.properties, 'reaction_package': m.fs.reactions, 'material_balance_type': MaterialBalanceType.componentTotal, 'energy_balance_type': EnergyBalanceType.enthalpyTotal, 'momentum_balance_type': MomentumBalanceType.none, 'has_heat_of_reaction': True }) m.fs.mixer = Mixer( default={ 'property_package': m.fs.properties, 'material_balance_type': MaterialBalanceType.componentTotal, 'momentum_mixing_type': MomentumMixingType.none, 'num_inlets': 2, 'inlet_list': ['S_inlet', 'E_inlet'] }) # Allegedly the proper energy balance is being used... # Time discretization if not steady: disc = TransformationFactory('dae.collocation') disc.apply_to(m, wrt=m.fs.time, nfe=ntfe, ncp=ntcp, scheme='LAGRANGE-RADAU') # Fix geometry variables m.fs.cstr.volume[0].fix(1.0) # Fix initial conditions: if not steady: for p, j in m.fs.properties.phase_list * m.fs.properties.component_list: if j == 'Solvent': continue m.fs.cstr.control_volume.material_holdup[0, p, j].fix(0.001) # Note: Model does not solve when initial conditions are empty tank m.fs.mixer.E_inlet.conc_mol.fix(0) m.fs.mixer.S_inlet.conc_mol.fix(0) for t, j in m.fs.time * m.fs.properties.component_list: if j == 'E': m.fs.mixer.E_inlet.conc_mol[t, j].fix(inlet_E) elif j == 'S': m.fs.mixer.S_inlet.conc_mol[t, j].fix(inlet_S) m.fs.mixer.E_inlet.flow_vol.fix(0.1) m.fs.mixer.S_inlet.flow_vol.fix(2.1) m.fs.mixer.E_inlet.conc_mol[:, 'Solvent'].fix(1.) m.fs.mixer.S_inlet.conc_mol[:, 'Solvent'].fix(1.) m.fs.mixer.E_inlet.temperature.fix(290) m.fs.mixer.S_inlet.temperature.fix(310) m.fs.inlet = Arc(source=m.fs.mixer.outlet, destination=m.fs.cstr.inlet) # This constraint is in lieu of tracking the CSTR's level and allowing # the outlet flow rate to be another degree of freedom. # ^ Not sure how to do this in IDAES. @m.fs.cstr.Constraint(m.fs.time, doc='Total flow rate balance') def total_flow_balance(cstr, t): return (cstr.inlet.flow_vol[t] == cstr.outlet.flow_vol[t]) # Specify initial condition for energy if not steady: m.fs.cstr.control_volume.energy_holdup[m.fs.time.first(), 'aq'].fix(300) TransformationFactory('network.expand_arcs').apply_to(m.fs) if bounds: m.fs.mixer.E_inlet.flow_vol.setlb(0.01) m.fs.mixer.E_inlet.flow_vol.setub(1.0) m.fs.mixer.S_inlet.flow_vol.setlb(0.5) m.fs.mixer.S_inlet.flow_vol.setub(5.0) m.fs.cstr.control_volume.material_holdup.setlb(0) holdup = m.fs.cstr.control_volume.material_holdup for t in m.fs.time: holdup[t, 'aq', 'S'].setub(20) holdup[t, 'aq', 'E'].setub(1) holdup[t, 'aq', 'P'].setub(5) holdup[t, 'aq', 'C'].setub(5) return m
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
def build(): # flowsheet set up m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.properties = props.NaClParameterBlock() m.fs.costing = WaterTAPCosting() # unit models m.fs.feed = Feed(default={'property_package': m.fs.properties}) m.fs.S1 = Separator(default={ "property_package": m.fs.properties, "outlet_list": ['P1', 'PXR'] }) m.fs.P1 = Pump(default={'property_package': m.fs.properties}) m.fs.PXR = PressureExchanger(default={'property_package': m.fs.properties}) m.fs.P2 = Pump(default={'property_package': m.fs.properties}) m.fs.M1 = Mixer( default={ "property_package": m.fs.properties, "momentum_mixing_type": MomentumMixingType.equality, # booster pump will match pressure "inlet_list": ['P1', 'P2'] }) m.fs.RO = ReverseOsmosis0D( default={ "property_package": m.fs.properties, "has_pressure_change": True, "pressure_change_type": PressureChangeType.calculated, "mass_transfer_coefficient": MassTransferCoefficient.calculated, "concentration_polarization_type": ConcentrationPolarizationType.calculated, }) m.fs.product = Product(default={'property_package': m.fs.properties}) m.fs.disposal = Product(default={'property_package': m.fs.properties}) # costing m.fs.costing.cost_flow( pyunits.convert(m.fs.P1.work_mechanical[0], to_units=pyunits.kW), "electricity") m.fs.costing.cost_flow( pyunits.convert(m.fs.P2.work_mechanical[0], to_units=pyunits.kW), "electricity") m.fs.P1.costing = UnitModelCostingBlock( default={"flowsheet_costing_block": m.fs.costing}) m.fs.P2.costing = UnitModelCostingBlock( default={"flowsheet_costing_block": m.fs.costing}) m.fs.RO.costing = UnitModelCostingBlock( default={"flowsheet_costing_block": m.fs.costing}) m.fs.PXR.costing = UnitModelCostingBlock( default={"flowsheet_costing_block": m.fs.costing}) m.fs.costing.cost_process() m.fs.costing.add_LCOW(m.fs.product.properties[0].flow_vol) m.fs.costing.add_specific_energy_consumption( m.fs.product.properties[0].flow_vol) # connections m.fs.s01 = Arc(source=m.fs.feed.outlet, destination=m.fs.S1.inlet) m.fs.s02 = Arc(source=m.fs.S1.P1, destination=m.fs.P1.inlet) m.fs.s03 = Arc(source=m.fs.P1.outlet, destination=m.fs.M1.P1) m.fs.s04 = Arc(source=m.fs.M1.outlet, destination=m.fs.RO.inlet) m.fs.s05 = Arc(source=m.fs.RO.permeate, destination=m.fs.product.inlet) m.fs.s06 = Arc(source=m.fs.RO.retentate, destination=m.fs.PXR.high_pressure_inlet) m.fs.s07 = Arc(source=m.fs.PXR.high_pressure_outlet, destination=m.fs.disposal.inlet) m.fs.s08 = Arc(source=m.fs.S1.PXR, destination=m.fs.PXR.low_pressure_inlet) m.fs.s09 = Arc(source=m.fs.PXR.low_pressure_outlet, destination=m.fs.P2.inlet) m.fs.s10 = Arc(source=m.fs.P2.outlet, destination=m.fs.M1.P2) TransformationFactory("network.expand_arcs").apply_to(m) # scaling # set default property values m.fs.properties.set_default_scaling('flow_mass_phase_comp', 1, index=('Liq', 'H2O')) m.fs.properties.set_default_scaling('flow_mass_phase_comp', 1e2, index=('Liq', 'NaCl')) # set unit model values iscale.set_scaling_factor(m.fs.P1.control_volume.work, 1e-3) iscale.set_scaling_factor(m.fs.P2.control_volume.work, 1e-3) iscale.set_scaling_factor(m.fs.PXR.low_pressure_side.work, 1e-3) iscale.set_scaling_factor(m.fs.PXR.high_pressure_side.work, 1e-3) # touch properties used in specifying and initializing the model m.fs.feed.properties[0].flow_vol_phase['Liq'] m.fs.feed.properties[0].mass_frac_phase_comp['Liq', 'NaCl'] m.fs.S1.mixed_state[0].mass_frac_phase_comp m.fs.S1.PXR_state[0].flow_vol_phase['Liq'] # unused scaling factors needed by IDAES base costing module # calculate and propagate scaling factors iscale.calculate_scaling_factors(m) return m
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, "material_balance_type": config.material_balance_type, "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)
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, "material_balance_type": config.material_balance_type, "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, ) 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 ) 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=idaeslog.NOTSET, solver="ipopt", optarg={"tol": 1e-6, "max_iter": 35}, copy_disconneted_flow=True, ): """ Initialize """ # 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 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) else: if copy_disconneted_flow: for t in stages[i].stages[i].inlet.flow_mol: stages[i].inlet.flow_mol[t] = \ prev_port.flow_mol[t] 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 for k in [1, 2]: # 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() 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) for t in self.flowsheet().time: self.inlet_split.inlet.flow_mol[ t ].value = self.outlet_stage.inlet.flow_mol[t].value from_json(self, sd=istate, wts=sp)
def create_model(): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) # Add properties for PEM m.fs.PEM_properties = GenericParameterBlock(default=configuration) # Add properties for hydrogen turbine m.fs.h2turbine_props = GenericParameterBlock(default=configuration1) m.fs.reaction_params = reaction_props.H2ReactionParameterBlock( default={"property_package": m.fs.h2turbine_props}) # Add the PEM electrolyzer unit m.fs.pem = PEM_Electrolyzer( default={"property_package": m.fs.PEM_properties}) # Add translator block m.fs.translator = Translator( default={ "inlet_property_package": m.fs.PEM_properties, "outlet_property_package": m.fs.h2turbine_props }) # Add mixer block m.fs.mixer = Mixer( default={ "property_package": m.fs.h2turbine_props, "inlet_list": ["air_feed", "hydrogen_feed"] }) # Add the hydrogen turbine m.fs.h2_turbine = HydrogenTurbine( default={ "property_package": m.fs.h2turbine_props, "reaction_package": m.fs.reaction_params }) m.fs.H2_mass = 2.016 / 1000 m.fs.H2_production = Expression(expr=m.fs.pem.outlet.flow_mol[0] * m.fs.H2_mass) # Add translator constraints # Set hydrogen flow and mole frac m.fs.translator.eq_flow_hydrogen = Constraint( expr=m.fs.translator.inlet.flow_mol[0] == m.fs.translator.outlet.flow_mol[0]) m.fs.translator.mole_frac_hydrogen = Constraint( expr=m.fs.translator.outlet.mole_frac_comp[0, "hydrogen"] == 0.99) m.fs.translator.eq_temperature = Constraint( expr=m.fs.translator.inlet.temperature[0] == m.fs.translator.outlet.temperature[0]) m.fs.translator.eq_pressure = Constraint( expr=m.fs.translator.inlet.pressure[0] == m.fs.translator.outlet.pressure[0]) m.fs.translator.outlet.mole_frac_comp[0, "oxygen"].fix(0.01 / 4) m.fs.translator.outlet.mole_frac_comp[0, "argon"].fix(0.01 / 4) m.fs.translator.outlet.mole_frac_comp[0, "nitrogen"].fix(0.01 / 4) m.fs.translator.outlet.mole_frac_comp[0, "water"].fix(0.01 / 4) # add arcs m.fs.pem_to_translator = Arc(source=m.fs.pem.outlet, destination=m.fs.translator.inlet) # add arcs m.fs.translator_to_mixer = Arc(source=m.fs.translator.outlet, destination=m.fs.mixer.hydrogen_feed) # add arcs m.fs.mixer_to_turbine = Arc(source=m.fs.mixer.outlet, destination=m.fs.h2_turbine.compressor.inlet) # expand arcs TransformationFactory("network.expand_arcs").apply_to(m) return m
def build(erd_type=None): # flowsheet set up m = ConcreteModel() m.db = Database() m.erd_type = erd_type m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.prop_prtrt = prop_ZO.WaterParameterBlock( default={"solute_list": ["tds", "tss"]} ) density = 1023.5 * pyunits.kg / pyunits.m**3 m.fs.prop_prtrt.dens_mass_default = density m.fs.prop_psttrt = prop_ZO.WaterParameterBlock(default={"solute_list": ["tds"]}) m.fs.prop_desal = prop_SW.SeawaterParameterBlock() # block structure prtrt = m.fs.pretreatment = Block() desal = m.fs.desalination = Block() psttrt = m.fs.posttreatment = Block() # unit models m.fs.feed = FeedZO(default={"property_package": m.fs.prop_prtrt}) # pretreatment prtrt.intake = SWOnshoreIntakeZO(default={"property_package": m.fs.prop_prtrt}) prtrt.ferric_chloride_addition = ChemicalAdditionZO( default={ "property_package": m.fs.prop_prtrt, "database": m.db, "process_subtype": "ferric_chloride", } ) prtrt.chlorination = ChlorinationZO( default={"property_package": m.fs.prop_prtrt, "database": m.db} ) prtrt.static_mixer = StaticMixerZO( default={"property_package": m.fs.prop_prtrt, "database": m.db} ) prtrt.storage_tank_1 = StorageTankZO( default={"property_package": m.fs.prop_prtrt, "database": m.db} ) prtrt.media_filtration = MediaFiltrationZO( default={"property_package": m.fs.prop_prtrt, "database": m.db} ) prtrt.backwash_handling = BackwashSolidsHandlingZO( default={"property_package": m.fs.prop_prtrt, "database": m.db} ) prtrt.anti_scalant_addition = ChemicalAdditionZO( default={ "property_package": m.fs.prop_prtrt, "database": m.db, "process_subtype": "anti-scalant", } ) prtrt.cartridge_filtration = CartridgeFiltrationZO( default={"property_package": m.fs.prop_prtrt, "database": m.db} ) # desalination desal.P1 = Pump(default={"property_package": m.fs.prop_desal}) desal.RO = ReverseOsmosis0D( default={ "property_package": m.fs.prop_desal, "has_pressure_change": True, "pressure_change_type": PressureChangeType.calculated, "mass_transfer_coefficient": MassTransferCoefficient.calculated, "concentration_polarization_type": ConcentrationPolarizationType.calculated, } ) desal.RO.width.setub(5000) desal.RO.area.setub(20000) if erd_type == "pressure_exchanger": desal.S1 = Separator( default={"property_package": m.fs.prop_desal, "outlet_list": ["P1", "PXR"]} ) desal.M1 = Mixer( default={ "property_package": m.fs.prop_desal, "momentum_mixing_type": MomentumMixingType.equality, # booster pump will match pressure "inlet_list": ["P1", "P2"], } ) desal.PXR = PressureExchanger(default={"property_package": m.fs.prop_desal}) desal.P2 = Pump(default={"property_package": m.fs.prop_desal}) elif erd_type == "pump_as_turbine": desal.ERD = EnergyRecoveryDevice(default={"property_package": m.fs.prop_desal}) else: raise ConfigurationError( "erd_type was {}, but can only " "be pressure_exchanger or pump_as_turbine" "".format(erd_type) ) # posttreatment psttrt.storage_tank_2 = StorageTankZO( default={"property_package": m.fs.prop_psttrt, "database": m.db} ) psttrt.uv_aop = UVAOPZO( default={ "property_package": m.fs.prop_psttrt, "database": m.db, "process_subtype": "hydrogen_peroxide", } ) psttrt.co2_addition = CO2AdditionZO( default={"property_package": m.fs.prop_psttrt, "database": m.db} ) psttrt.lime_addition = ChemicalAdditionZO( default={ "property_package": m.fs.prop_psttrt, "database": m.db, "process_subtype": "lime", } ) psttrt.storage_tank_3 = StorageTankZO( default={"property_package": m.fs.prop_psttrt, "database": m.db} ) # product and disposal m.fs.municipal = MunicipalDrinkingZO( default={"property_package": m.fs.prop_psttrt, "database": m.db} ) m.fs.landfill = LandfillZO( default={"property_package": m.fs.prop_prtrt, "database": m.db} ) m.fs.disposal = Product(default={"property_package": m.fs.prop_desal}) # translator blocks m.fs.tb_prtrt_desal = Translator( default={ "inlet_property_package": m.fs.prop_prtrt, "outlet_property_package": m.fs.prop_desal, } ) @m.fs.tb_prtrt_desal.Constraint(["H2O", "tds"]) def eq_flow_mass_comp(blk, j): if j == "tds": jj = "TDS" else: jj = j return ( blk.properties_in[0].flow_mass_comp[j] == blk.properties_out[0].flow_mass_phase_comp["Liq", jj] ) m.fs.tb_desal_psttrt = Translator( default={ "inlet_property_package": m.fs.prop_desal, "outlet_property_package": m.fs.prop_psttrt, } ) @m.fs.tb_desal_psttrt.Constraint(["H2O", "TDS"]) def eq_flow_mass_comp(blk, j): if j == "TDS": jj = "tds" else: jj = j return ( blk.properties_in[0].flow_mass_phase_comp["Liq", j] == blk.properties_out[0].flow_mass_comp[jj] ) # connections m.fs.s_feed = Arc(source=m.fs.feed.outlet, destination=prtrt.intake.inlet) prtrt.s01 = Arc( source=prtrt.intake.outlet, destination=prtrt.ferric_chloride_addition.inlet ) prtrt.s02 = Arc( source=prtrt.ferric_chloride_addition.outlet, destination=prtrt.chlorination.inlet, ) prtrt.s03 = Arc( source=prtrt.chlorination.treated, destination=prtrt.static_mixer.inlet ) prtrt.s04 = Arc( source=prtrt.static_mixer.outlet, destination=prtrt.storage_tank_1.inlet ) prtrt.s05 = Arc( source=prtrt.storage_tank_1.outlet, destination=prtrt.media_filtration.inlet ) prtrt.s06 = Arc( source=prtrt.media_filtration.byproduct, destination=prtrt.backwash_handling.inlet, ) prtrt.s07 = Arc( source=prtrt.media_filtration.treated, destination=prtrt.anti_scalant_addition.inlet, ) prtrt.s08 = Arc( source=prtrt.anti_scalant_addition.outlet, destination=prtrt.cartridge_filtration.inlet, ) m.fs.s_prtrt_tb = Arc( source=prtrt.cartridge_filtration.treated, destination=m.fs.tb_prtrt_desal.inlet ) m.fs.s_landfill = Arc( source=prtrt.backwash_handling.byproduct, destination=m.fs.landfill.inlet ) if erd_type == "pressure_exchanger": m.fs.s_tb_desal = Arc( source=m.fs.tb_prtrt_desal.outlet, destination=desal.S1.inlet ) desal.s01 = Arc(source=desal.S1.P1, destination=desal.P1.inlet) desal.s02 = Arc(source=desal.P1.outlet, destination=desal.M1.P1) desal.s03 = Arc(source=desal.M1.outlet, destination=desal.RO.inlet) desal.s04 = Arc( source=desal.RO.retentate, destination=desal.PXR.high_pressure_inlet ) desal.s05 = Arc(source=desal.S1.PXR, destination=desal.PXR.low_pressure_inlet) desal.s06 = Arc( source=desal.PXR.low_pressure_outlet, destination=desal.P2.inlet ) desal.s07 = Arc(source=desal.P2.outlet, destination=desal.M1.P2) m.fs.s_disposal = Arc( source=desal.PXR.high_pressure_outlet, destination=m.fs.disposal.inlet ) elif erd_type == "pump_as_turbine": m.fs.s_tb_desal = Arc( source=m.fs.tb_prtrt_desal.outlet, destination=desal.P1.inlet ) desal.s01 = Arc(source=desal.P1.outlet, destination=desal.RO.inlet) desal.s02 = Arc(source=desal.RO.retentate, destination=desal.ERD.inlet) m.fs.s_disposal = Arc(source=desal.ERD.outlet, destination=m.fs.disposal.inlet) m.fs.s_desal_tb = Arc( source=desal.RO.permeate, destination=m.fs.tb_desal_psttrt.inlet ) m.fs.s_tb_psttrt = Arc( source=m.fs.tb_desal_psttrt.outlet, destination=psttrt.storage_tank_2.inlet ) psttrt.s01 = Arc( source=psttrt.storage_tank_2.outlet, destination=psttrt.uv_aop.inlet ) psttrt.s02 = Arc( source=psttrt.uv_aop.treated, destination=psttrt.co2_addition.inlet ) psttrt.s03 = Arc( source=psttrt.co2_addition.outlet, destination=psttrt.lime_addition.inlet ) psttrt.s04 = Arc( source=psttrt.lime_addition.outlet, destination=psttrt.storage_tank_3.inlet ) m.fs.s_municipal = Arc( source=psttrt.storage_tank_3.outlet, destination=m.fs.municipal.inlet ) TransformationFactory("network.expand_arcs").apply_to(m) # scaling # set default property values m.fs.prop_desal.set_default_scaling( "flow_mass_phase_comp", 1e-3, index=("Liq", "H2O") ) m.fs.prop_desal.set_default_scaling( "flow_mass_phase_comp", 1e-1, index=("Liq", "TDS") ) # set unit model values iscale.set_scaling_factor(desal.P1.control_volume.work, 1e-5) iscale.set_scaling_factor(desal.RO.area, 1e-4) if erd_type == "pressure_exchanger": iscale.set_scaling_factor(desal.P2.control_volume.work, 1e-5) iscale.set_scaling_factor(desal.PXR.low_pressure_side.work, 1e-5) iscale.set_scaling_factor(desal.PXR.high_pressure_side.work, 1e-5) elif erd_type == "pump_as_turbine": iscale.set_scaling_factor(desal.ERD.control_volume.work, 1e-5) # calculate and propagate scaling factors iscale.calculate_scaling_factors(m) return m
def build_desalination( m, has_desal_feed=False, is_twostage=False, has_ERD=False, RO_type="0D", RO_base="TDS", RO_level="simple", ): """ Builds RO desalination including specified feed and auxiliary equipment. Args: has_desal_feed: True or False, default = False, if True a feed block is created and specified to the standard feed RO_type: 'Sep', '0D', or 1D, default = '0D' RO_level: 'simple' or 'detailed', default = 'simple' RO_base: 'TDS' only, default = 'ion' """ desal_port = {} prop = property_models.get_prop(m, base=RO_base) if has_desal_feed: # build feed feed_block.build_feed(m, base=RO_base) # build RO if RO_type == "Sep": unit_separator.build_SepRO(m, base=RO_base) elif RO_type == "0D": unit_0DRO.build_RO(m, base="TDS", level=RO_level) elif RO_type == "1D": unit_1DRO.build_RO(m, base="TDS", level=RO_level) else: raise ValueError( "Unexpected model type {RO_type} provided to build_desalination" "".format(RO_type=RO_type)) if is_twostage: if RO_type == "0D": unit_0DRO.build_RO(m, base="TDS", level=RO_level, name_str="RO2") elif RO_type == "1D": unit_1DRO.build_RO(m, base="TDS", level=RO_level, name_str="RO2") else: raise ValueError( "Unexpected model type {RO_type} provided to build_desalination when is_twostage is True" "".format(RO_type=RO_type)) if has_ERD: if RO_type == "Sep": raise ValueError( "Unexpected model type {RO_type} provided to build_desalination when has_ERD is True" "".format(RO_type=RO_type)) m.fs.ERD = EnergyRecoveryDevice(default={"property_package": prop}) # auxiliary units if RO_type == "Sep": # build auxiliary units (none) # connect models if has_desal_feed: m.fs.s_desal_feed_RO = Arc(source=m.fs.feed.outlet, destination=m.fs.RO.inlet) # specify (RO already specified) # inlet/outlet ports for pretreatment if not has_desal_feed: desal_port["in"] = m.fs.RO.inlet elif RO_type == "0D" or RO_type == "1D": # build auxiliary units m.fs.pump_RO = Pump(default={"property_package": prop}) if is_twostage: m.fs.pump_RO2 = Pump(default={"property_package": prop}) m.fs.mixer_permeate = Mixer(default={ "property_package": prop, "inlet_list": ["RO", "RO2"] }) # connect models if has_desal_feed: m.fs.s_desal_feed_pumpRO = Arc(source=m.fs.feed.outlet, destination=m.fs.pump_RO.inlet) m.fs.s_desal_pumpRO_RO = Arc(source=m.fs.pump_RO.outlet, destination=m.fs.RO.inlet) if is_twostage: m.fs.s_desal_RO_pumpRO2 = Arc(source=m.fs.RO.retentate, destination=m.fs.pump_RO2.inlet) m.fs.s_desal_pumpRO2_RO2 = Arc(source=m.fs.pump_RO2.outlet, destination=m.fs.RO2.inlet) m.fs.s_desal_permeateRO_mixer = Arc( source=m.fs.RO.permeate, destination=m.fs.mixer_permeate.RO) m.fs.s_desal_permeateRO2_mixer = Arc( source=m.fs.RO2.permeate, destination=m.fs.mixer_permeate.RO2) if has_ERD: if is_twostage: m.fs.s_desal_RO2_ERD = Arc(source=m.fs.RO2.retentate, destination=m.fs.ERD.inlet) else: m.fs.s_desal_RO_ERD = Arc(source=m.fs.RO.retentate, destination=m.fs.ERD.inlet) # specify (RO already specified, Pump 2 DOF, ERD 2 DOF) m.fs.pump_RO.efficiency_pump.fix(0.80) m.fs.pump_RO.control_volume.properties_out[0].pressure.fix(50e5) if is_twostage: m.fs.pump_RO2.efficiency_pump.fix(0.80) m.fs.pump_RO2.control_volume.properties_out[0].pressure.fix(55e5) if has_ERD: m.fs.ERD.efficiency_pump.fix(0.95) m.fs.ERD.outlet.pressure[0].fix(101325) # inlet/outlet ports for pretreatment if not has_desal_feed: desal_port["in"] = m.fs.pump_RO.inlet if is_twostage: desal_port["out"] = m.fs.mixer_permeate.outlet if has_ERD: desal_port["waste"] = m.fs.ERD.outlet else: desal_port["waste"] = m.fs.RO2.retentate else: desal_port["out"] = m.fs.RO.permeate if has_ERD: desal_port["waste"] = m.fs.ERD.outlet else: desal_port["waste"] = m.fs.RO.retentate return desal_port
def build_ne_flowsheet(m, **kwargs): """ This function builds the entire nuclear flowsheet by adding the required models and arcs connecting the models. """ m.fs = FlowsheetBlock(default={"dynamic": False}) # Load thermodynamic and reaction packages m.fs.h2ideal_props = GenericParameterBlock(default=h2_ideal_config) m.fs.h2turbine_props = GenericParameterBlock(default=hturbine_config) m.fs.reaction_params = h2_reaction_props.H2ReactionParameterBlock( default={"property_package": m.fs.h2turbine_props}) # Add electrical splitter m.fs.np_power_split = ElectricalSplitter(default={ "num_outlets": 2, "outlet_list": ["np_to_grid", "np_to_pem"]}) # Add PEM electrolyzer m.fs.pem = PEM_Electrolyzer(default={ "property_package": m.fs.h2ideal_props}) # Add hydrogen tank m.fs.h2_tank = SimpleHydrogenTank(default={ "property_package": m.fs.h2ideal_props}) # Add translator block m.fs.translator = Translator(default={ "inlet_property_package": m.fs.h2ideal_props, "outlet_property_package": m.fs.h2turbine_props}) # Add translator block constraints m.fs.translator.eq_flow_hydrogen = Constraint( expr=m.fs.translator.inlet.flow_mol[0] == m.fs.translator.outlet.flow_mol[0]) m.fs.translator.eq_temperature = Constraint( expr=m.fs.translator.inlet.temperature[0] == m.fs.translator.outlet.temperature[0]) m.fs.translator.eq_pressure = Constraint( expr=m.fs.translator.inlet.pressure[0] == m.fs.translator.outlet.pressure[0]) # Add mixer block # using minimize pressure for all inlets and outlet of the mixer # because pressure of inlets is already fixed in flowsheet, # using equality will over-constrain m.fs.mixer = Mixer(default={ "momentum_mixing_type": MomentumMixingType.minimize, "property_package": m.fs.h2turbine_props, "inlet_list": ["air_feed", "hydrogen_feed"]}) # Add hydrogen turbine m.fs.h2_turbine = HydrogenTurbine( default={"property_package": m.fs.h2turbine_props, "reaction_package": m.fs.reaction_params}) """ Connect the individual blocks via Arcs """ # Connect the electrical splitter and PEM m.fs.arc_np_to_pem = Arc( source=m.fs.np_power_split.np_to_pem_port, destination=m.fs.pem.electricity_in ) # Connect the pem electrolyzer and h2 tank m.fs.arc_pem_to_h2_tank = Arc( source=m.fs.pem.outlet, destination=m.fs.h2_tank.inlet ) # Connect h2 tank and translator m.fs.arc_h2_tank_to_translator = Arc( source=m.fs.h2_tank.outlet_to_turbine, destination=m.fs.translator.inlet ) # Connect translator and mixer m.fs.arc_translator_to_mixer = Arc( source=m.fs.translator.outlet, destination=m.fs.mixer.hydrogen_feed ) # Connect mixer and h2 turbine m.fs.arc_mixer_to_h2_turbine = Arc( source=m.fs.mixer.outlet, destination=m.fs.h2_turbine.compressor.inlet ) TransformationFactory("network.expand_arcs").apply_to(m) return m
def build_pretreatment_NF(m, has_bypass=True, NF_type="ZO", NF_base="ion"): """ Builds NF pretreatment including specified feed and auxiliary equipment. Arguments: has_bypass: True or False, default = True NF_type: 'Sep' or 'ZO', default = 'ZO' NF_base: 'ion' or 'salt', default = 'ion' """ pretrt_port = {} prop = property_models.get_prop(m, base=NF_base) # build feed feed_block.build_feed(m, base=NF_base) # build NF if NF_type == "Sep": unit_separator.build_SepNF(m, base=NF_base) elif NF_type == "ZO": unit_ZONF.build_ZONF(m, base=NF_base) m.fs.pump_NF = Pump(default={"property_package": prop}) else: raise ValueError( "Unexpected model type {NF_type} provided to build_NF_no_bypass" "".format(NF_type=NF_type) ) if has_bypass: # build auxiliary units m.fs.splitter = Separator( default={ "property_package": prop, "outlet_list": ["pretreatment", "bypass"], "split_basis": SplittingType.totalFlow, "energy_split_basis": EnergySplittingType.equal_temperature, } ) m.fs.mixer = Mixer( default={"property_package": prop, "inlet_list": ["pretreatment", "bypass"]} ) # connect models m.fs.s_pretrt_feed_splitter = Arc( source=m.fs.feed.outlet, destination=m.fs.splitter.inlet ) m.fs.s_pretrt_splitter_mixer = Arc( source=m.fs.splitter.bypass, destination=m.fs.mixer.bypass ) if NF_type == "ZO": m.fs.s_pretrt_splitter_pumpNF = Arc( source=m.fs.splitter.pretreatment, destination=m.fs.pump_NF.inlet ) m.fs.s_pretrt_pumpNF_NF = Arc( source=m.fs.pump_NF.outlet, destination=m.fs.NF.inlet ) else: m.fs.s_pretrt_splitter_NF = Arc( source=m.fs.splitter.pretreatment, destination=m.fs.NF.inlet ) m.fs.s_pretrt_NF_mixer = Arc( source=m.fs.NF.permeate, destination=m.fs.mixer.pretreatment ) # specify (NF and feed is already specified, mixer has 0 DOF, splitter has 1 DOF, NF pump has 2 DOF) # splitter m.fs.splitter.split_fraction[0, "bypass"].fix(0.1) if NF_type == "ZO": m.fs.pump_NF.efficiency_pump.fix(0.80) m.fs.pump_NF.control_volume.properties_out[0].pressure.fix(4e5) # inlet/outlet ports for pretreatment pretrt_port["out"] = m.fs.mixer.outlet pretrt_port["waste"] = m.fs.NF.retentate else: # no bypass # build auxiliary units (none) # connect models if NF_type == "ZO": m.fs.s_pretrt_feed_pumpNF = Arc( source=m.fs.feed.outlet, destination=m.fs.pump_NF.inlet ) m.fs.s_pretrt_pumpNF_NF = Arc( source=m.fs.pump_NF.outlet, destination=m.fs.NF.inlet ) # TODO: should source be m.fs.pump_NF.outlet? Double-check here and other arcs with pump_NF else: m.fs.s_pretrt_feed_NF = Arc( source=m.fs.feed.outlet, destination=m.fs.NF.inlet ) # specify (NF and feed are already specified, NF pump has 2 DOF) if NF_type == "ZO": m.fs.pump_NF.efficiency_pump.fix(0.80) m.fs.pump_NF.control_volume.properties_out[0].pressure.fix(4e5) # inlet/outlet ports for pretreatment pretrt_port["out"] = m.fs.NF.permeate pretrt_port["waste"] = m.fs.NF.retentate return pretrt_port
def make_model(horizon=6, ntfe=60, ntcp=2, inlet_E=11.91, inlet_S=12.92): time_set = [0, horizon] m = ConcreteModel(name='CSTR with level control') m.fs = FlowsheetBlock(default={'dynamic': True, 'time_set': time_set}) m.fs.properties = AqueousEnzymeParameterBlock() m.fs.reactions = EnzymeReactionParameterBlock( default={'property_package': m.fs.properties}) m.fs.cstr = CSTR(default={'has_holdup': True, 'property_package': m.fs.properties, 'reaction_package': m.fs.reactions, 'material_balance_type': MaterialBalanceType.componentTotal, 'energy_balance_type': EnergyBalanceType.enthalpyTotal, 'momentum_balance_type': MomentumBalanceType.none, 'has_heat_of_reaction': True}) # MomentumBalanceType.none used because the property package doesn't # include pressure. m.fs.mixer = Mixer(default={ 'property_package': m.fs.properties, 'material_balance_type': MaterialBalanceType.componentTotal, 'momentum_mixing_type': MomentumMixingType.none, # MomentumMixingType.none used because the property package doesn't # include pressure. 'num_inlets': 2, 'inlet_list': ['S_inlet', 'E_inlet']}) # Allegedly the proper energy balance is being used... # Time discretization disc = TransformationFactory('dae.collocation') disc.apply_to(m, wrt=m.fs.time, nfe=ntfe, ncp=ntcp, scheme='LAGRANGE-RADAU') m.fs.pid = PIDBlock(default={'pv': m.fs.cstr.volume, 'output': m.fs.cstr.outlet.flow_vol, 'upper': 5.0, 'lower': 0.5, 'calculate_initial_integral': True, # ^ Why would initial integral be calculated # to be nonzero? 'pid_form': PIDForm.velocity}) m.fs.pid.gain.fix(-1.0) m.fs.pid.time_i.fix(0.1) m.fs.pid.time_d.fix(0.0) m.fs.pid.setpoint.fix(1.0) # Fix initial condition for volume: m.fs.cstr.volume.unfix() m.fs.cstr.volume[m.fs.time.first()].fix(1.0) # Fix initial conditions for other variables: for p, j in m.fs.properties.phase_list*m.fs.properties.component_list: if j == 'Solvent': continue m.fs.cstr.control_volume.material_holdup[0, p, j].fix(0.001) # Note: Model does not solve when initial conditions are empty tank m.fs.cstr.control_volume.energy_holdup[m.fs.time.first(), 'aq'].fix(300) m.fs.mixer.E_inlet.conc_mol.fix(0) m.fs.mixer.S_inlet.conc_mol.fix(0) m.fs.mixer.E_inlet.conc_mol[:,'Solvent'].fix(1.) m.fs.mixer.S_inlet.conc_mol[:,'Solvent'].fix(1.) for t, j in m.fs.time*m.fs.properties.component_list: if j == 'E': m.fs.mixer.E_inlet.conc_mol[t, j].fix(inlet_E) elif j == 'S': m.fs.mixer.S_inlet.conc_mol[t, j].fix(inlet_S) m.fs.mixer.E_inlet.flow_vol.fix(0.1) m.fs.mixer.S_inlet.flow_vol.fix(2.1) # Specify a perturbation to substrate flow rate: for t in m.fs.time: if t < horizon/4: continue else: m.fs.mixer.S_inlet.flow_vol[t].fix(3.0) m.fs.mixer.E_inlet.temperature.fix(290) m.fs.mixer.S_inlet.temperature.fix(310) m.fs.inlet = Arc(source=m.fs.mixer.outlet, destination=m.fs.cstr.inlet) # Fix "initial condition" for outlet flow rate, as here it cannot be # specified by the PID controller m.fs.cstr.outlet.flow_vol[m.fs.time.first()].fix(2.2) TransformationFactory('network.expand_arcs').apply_to(m.fs) return m
def add_unit_models(m): fs = m.fs_main.fs_blr prop_water = m.fs_main.prop_water prop_gas = m.fs_main.prop_gas fs.num_mills = pyo.Var() fs.num_mills.fix(4) # 14 waterwall zones fs.ww_zones = pyo.RangeSet(14) # boiler based on surrogate fs.aBoiler = BoilerSurrogate(default={"dynamic": False, "side_1_property_package": prop_gas, "side_2_property_package": prop_gas, "has_heat_transfer": False, "has_pressure_change": False, "has_holdup": False}) # model a drum by a WaterFlash, a Mixer and a Drum model fs.aFlash = WaterFlash(default={"dynamic": False, "property_package": prop_water, "has_phase_equilibrium": False, "has_heat_transfer": False, "has_pressure_change": False}) fs.aMixer = HelmMixer(default={"dynamic": False, "property_package": prop_water, "momentum_mixing_type": MomentumMixingType.equality, "inlet_list": ["FeedWater", "SatWater"]}) fs.aDrum = Drum1D(default={"property_package": prop_water, "has_holdup": True, "has_heat_transfer": True, "has_pressure_change": True, "finite_elements": 4, "inside_diameter": 1.778, "thickness": 0.127}) fs.blowdown_split = HelmSplitter( default={ "dynamic": False, "property_package": prop_water, "outlet_list": ["FW_Downcomer", "FW_Blowdown"], } ) # downcomer fs.aDowncomer = Downcomer(default={ "dynamic": False, "property_package": prop_water, "has_holdup": True, "has_heat_transfer": True, "has_pressure_change": True}) # 14 WaterwallSection units fs.Waterwalls = WaterwallSection(fs.ww_zones, default={ "has_holdup": True, "property_package": prop_water, "has_equilibrium_reactions": False, "has_heat_of_reaction": False, "has_heat_transfer": True, "has_pressure_change": True}) # roof superheater fs.aRoof = SteamHeater(default={ "dynamic": False, "property_package": prop_water, "has_holdup": True, "has_equilibrium_reactions": False, "has_heat_of_reaction": False, "has_heat_transfer": True, "has_pressure_change": True, "single_side_only" : True}) # platen superheater fs.aPlaten = SteamHeater(default={ "dynamic": False, "property_package": prop_water, "has_holdup": True, "has_equilibrium_reactions": False, "has_heat_of_reaction": False, "has_heat_transfer": True, "has_pressure_change": True, "single_side_only" : False}) # 1st reheater fs.aRH1 = HeatExchangerCrossFlow2D(default={ "tube_side":{"property_package": prop_water, "has_holdup": False, "has_pressure_change": True}, "shell_side":{"property_package": prop_gas, "has_holdup": False, "has_pressure_change": True}, "finite_elements": 4, "flow_type": "counter_current", "tube_arrangement": "in-line", "tube_side_water_phase": "Vap", "has_radiation": True, "radial_elements": 5, "inside_diameter": 2.202*0.0254, "thickness": 0.149*0.0254}) # 2nd reheater fs.aRH2 = HeatExchangerCrossFlow2D(default={ "tube_side":{"property_package": prop_water, "has_holdup": False, "has_pressure_change": True}, "shell_side":{"property_package": prop_gas, "has_holdup": False, "has_pressure_change": True}, "finite_elements": 2, "flow_type": "counter_current", "tube_arrangement": "in-line", "tube_side_water_phase": "Vap", "has_radiation": True, "radial_elements": 5, "inside_diameter": 2.217*0.0254, "thickness": 0.1415*0.0254}) # primary superheater fs.aPSH = HeatExchangerCrossFlow2D(default={ "tube_side":{"property_package": prop_water, "has_holdup": False, "has_pressure_change": True}, "shell_side":{"property_package": prop_gas, "has_holdup": False, "has_pressure_change": True}, "finite_elements": 5, "flow_type": "counter_current", "tube_arrangement": "in-line", "tube_side_water_phase": "Vap", "has_radiation": True, "radial_elements": 5, "inside_diameter": 1.45*0.0254, "thickness": 0.15*0.0254}) # economizer fs.aECON = HeatExchangerCrossFlow2D(default={ "tube_side":{"property_package": prop_water, "has_holdup": False, "has_pressure_change": True}, "shell_side":{"property_package": prop_gas, "has_holdup": False, "has_pressure_change": True}, "finite_elements": 5, "flow_type": "counter_current", "tube_arrangement": "in-line", "tube_side_water_phase": "Liq", "has_radiation": False, "radial_elements": 5, "inside_diameter": 1.452*0.0254, "thickness": 0.149*0.0254}) # water pipe from economizer outlet to drum fs.aPipe = WaterPipe(default={ "dynamic": False, "property_package": prop_water, "has_holdup": True, "has_heat_transfer": False, "has_pressure_change": True, "water_phase": 'Liq', "contraction_expansion_at_end": 'None'}) # a mixer to mix hot primary air with tempering air fs.Mixer_PA = Mixer( default={ "dynamic": False, "property_package": prop_gas, "momentum_mixing_type": MomentumMixingType.equality, "inlet_list": ["PA_inlet", "TA_inlet"], } ) # attemperator for main steam before platen SH fs.Attemp = HelmMixer( default={ "dynamic": False, "property_package": prop_water, "momentum_mixing_type": MomentumMixingType.equality, "inlet_list": ["Steam_inlet", "Water_inlet"], } ) # air preheater as three-stream heat exchanger with heat loss to ambient, # side_1: flue gas # side_2:PA (priamry air?) # side_3:PA (priamry air?) fs.aAPH = HeatExchangerWith3Streams( default={"dynamic": False, "side_1_property_package": prop_gas, "side_2_property_package": prop_gas, "side_3_property_package": prop_gas, "has_heat_transfer": True, "has_pressure_change": True, "has_holdup": False, "flow_type_side_2": "counter-current", "flow_type_side_3": "counter-current", } ) return m
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 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 build(number_of_stages=2): # ---building model--- m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = props.NaClParameterBlock() m.fs.costing = WaterTAPCosting() m.fs.NumberOfStages = Param(initialize=number_of_stages) m.fs.StageSet = RangeSet(m.fs.NumberOfStages) m.fs.LSRRO_StageSet = RangeSet(2, m.fs.NumberOfStages) m.fs.NonFinal_StageSet = RangeSet(m.fs.NumberOfStages-1) m.fs.feed = Feed(default={'property_package': m.fs.properties}) m.fs.product = Product(default={'property_package': m.fs.properties}) m.fs.disposal = Product(default={'property_package': m.fs.properties}) # Add the mixers m.fs.Mixers = Mixer(m.fs.NonFinal_StageSet, default={ "property_package": m.fs.properties, "momentum_mixing_type": MomentumMixingType.equality, # booster pump will match pressure "inlet_list": ['upstream', 'downstream']}) total_pump_work = 0 # Add the pumps m.fs.PrimaryPumps = Pump(m.fs.StageSet, default={"property_package": m.fs.properties}) for pump in m.fs.PrimaryPumps.values(): pump.costing = UnitModelCostingBlock(default={ "flowsheet_costing_block":m.fs.costing}) m.fs.costing.cost_flow(pyunits.convert(pump.work_mechanical[0], to_units=pyunits.kW), "electricity") # Add the equalizer pumps m.fs.BoosterPumps = Pump(m.fs.LSRRO_StageSet, default={"property_package": m.fs.properties}) for pump in m.fs.BoosterPumps.values(): pump.costing = UnitModelCostingBlock(default={ "flowsheet_costing_block":m.fs.costing}) m.fs.costing.cost_flow(pyunits.convert(pump.work_mechanical[0], to_units=pyunits.kW), "electricity") # Add the stages ROs m.fs.ROUnits = ReverseOsmosis0D(m.fs.StageSet, default={ "property_package": m.fs.properties, "has_pressure_change": True, "pressure_change_type": PressureChangeType.calculated, "mass_transfer_coefficient": MassTransferCoefficient.calculated, "concentration_polarization_type": ConcentrationPolarizationType.calculated}) for ro_unit in m.fs.ROUnits.values(): ro_unit.costing = UnitModelCostingBlock(default={ "flowsheet_costing_block":m.fs.costing}) # Add EnergyRecoveryDevice m.fs.EnergyRecoveryDevice = Pump(default={"property_package": m.fs.properties}) m.fs.EnergyRecoveryDevice.costing = UnitModelCostingBlock(default={ "flowsheet_costing_block":m.fs.costing, "costing_method_arguments":{"pump_type":PumpType.energy_recovery_device}}) m.fs.costing.cost_flow(pyunits.convert(m.fs.EnergyRecoveryDevice.work_mechanical[0], to_units=pyunits.kW), "electricity") # additional variables or expressions # system water recovery m.fs.water_recovery = Var( initialize=0.5, bounds=(0, 1), domain=NonNegativeReals, units=pyunits.dimensionless, doc='System Water Recovery') m.fs.eq_water_recovery = Constraint(expr=\ sum(m.fs.feed.flow_mass_phase_comp[0,'Liq',:]) * m.fs.water_recovery == \ sum(m.fs.product.flow_mass_phase_comp[0,'Liq',:]) ) # costing m.fs.costing.cost_process() product_flow_vol_total = m.fs.product.properties[0].flow_vol m.fs.costing.add_LCOW(product_flow_vol_total) m.fs.costing.add_specific_energy_consumption(product_flow_vol_total) # objective m.fs.objective = Objective(expr=m.fs.costing.LCOW) # connections # Connect the feed to the first pump m.fs.feed_to_pump = Arc(source=m.fs.feed.outlet, destination=m.fs.PrimaryPumps[1].inlet) # Connect the primary RO permeate to the product m.fs.primary_RO_to_product = Arc(source=m.fs.ROUnits[1].permeate, destination=m.fs.product.inlet) # Connect the Pump n to the Mixer n m.fs.pump_to_mixer = Arc(m.fs.NonFinal_StageSet, rule=lambda fs,n : {'source':fs.PrimaryPumps[n].outlet, 'destination':fs.Mixers[n].upstream}) # Connect the Mixer n to the Stage n m.fs.mixer_to_stage = Arc(m.fs.NonFinal_StageSet, rule=lambda fs,n : {'source':fs.Mixers[n].outlet, 'destination':fs.ROUnits[n].inlet}) # Connect the Stage n to the Pump n+1 m.fs.stage_to_pump = Arc(m.fs.NonFinal_StageSet, rule=lambda fs,n : {'source':fs.ROUnits[n].retentate, 'destination':fs.PrimaryPumps[n+1].inlet}) # Connect the Stage n to the Eq Pump n m.fs.stage_to_eq_pump = Arc(m.fs.LSRRO_StageSet, rule=lambda fs,n : {'source':fs.ROUnits[n].permeate, 'destination':fs.BoosterPumps[n].inlet}) # Connect the Eq Pump n to the Mixer n-1 m.fs.eq_pump_to_mixer = Arc(m.fs.LSRRO_StageSet, rule=lambda fs,n : {'source':fs.BoosterPumps[n].outlet, 'destination':fs.Mixers[n-1].downstream}) # Connect the Pump N to the Stage N last_stage = m.fs.StageSet.last() m.fs.pump_to_stage = Arc(source=m.fs.PrimaryPumps[last_stage].outlet, destination=m.fs.ROUnits[last_stage].inlet) # Connect Final Stage to EnergyRecoveryDevice Pump m.fs.stage_to_erd = Arc(source=m.fs.ROUnits[last_stage].retentate, destination=m.fs.EnergyRecoveryDevice.inlet) # Connect the EnergyRecoveryDevice to the disposal m.fs.erd_to_disposal = Arc(source=m.fs.EnergyRecoveryDevice.outlet, destination=m.fs.disposal.inlet) # additional bounding for b in m.component_data_objects(Block, descend_into=True): # NaCl solubility limit if hasattr(b, 'mass_frac_phase_comp'): b.mass_frac_phase_comp['Liq', 'NaCl'].setub(0.26) TransformationFactory("network.expand_arcs").apply_to(m) return m
def build(): # flowsheet set up m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.properties = props.NaClParameterBlock() financials.add_costing_param_block(m.fs) # unit models m.fs.feed = Feed(default={'property_package': m.fs.properties}) m.fs.S1 = Separator(default={ "property_package": m.fs.properties, "outlet_list": ['P1', 'PXR'] }) m.fs.P1 = Pump(default={'property_package': m.fs.properties}) m.fs.PXR = PressureExchanger(default={'property_package': m.fs.properties}) m.fs.P2 = Pump(default={'property_package': m.fs.properties}) m.fs.M1 = Mixer( default={ "property_package": m.fs.properties, "momentum_mixing_type": MomentumMixingType.equality, # booster pump will match pressure "inlet_list": ['P1', 'P2'] }) m.fs.RO = ReverseOsmosis0D(default={ "property_package": m.fs.properties, "has_pressure_change": True }) m.fs.product = Product(default={'property_package': m.fs.properties}) m.fs.disposal = Product(default={'property_package': m.fs.properties}) # additional variables or expressions feed_flow_vol_total = m.fs.feed.properties[0].flow_vol product_flow_vol_total = m.fs.product.properties[0].flow_vol m.fs.recovery = Expression(expr=product_flow_vol_total / feed_flow_vol_total) m.fs.annual_water_production = Expression(expr=pyunits.convert( product_flow_vol_total, to_units=pyunits.m**3 / pyunits.year) * m.fs.costing_param.load_factor) pump_power_total = m.fs.P1.work_mechanical[0] + m.fs.P2.work_mechanical[0] m.fs.specific_energy_consumption = Expression( expr=pyunits.convert(pump_power_total, to_units=pyunits.kW) / pyunits.convert(product_flow_vol_total, to_units=pyunits.m**3 / pyunits.hr)) # costing m.fs.P1.get_costing(module=financials, pump_type="High pressure") m.fs.P2.get_costing(module=financials, pump_type="High pressure") m.fs.RO.get_costing(module=financials) m.fs.PXR.get_costing(module=financials) financials.get_system_costing(m.fs) # connections m.fs.s01 = Arc(source=m.fs.feed.outlet, destination=m.fs.S1.inlet) m.fs.s02 = Arc(source=m.fs.S1.P1, destination=m.fs.P1.inlet) m.fs.s03 = Arc(source=m.fs.P1.outlet, destination=m.fs.M1.P1) m.fs.s04 = Arc(source=m.fs.M1.outlet, destination=m.fs.RO.inlet) m.fs.s05 = Arc(source=m.fs.RO.permeate, destination=m.fs.product.inlet) m.fs.s06 = Arc(source=m.fs.RO.retentate, destination=m.fs.PXR.high_pressure_inlet) m.fs.s07 = Arc(source=m.fs.PXR.high_pressure_outlet, destination=m.fs.disposal.inlet) m.fs.s08 = Arc(source=m.fs.S1.PXR, destination=m.fs.PXR.low_pressure_inlet) m.fs.s09 = Arc(source=m.fs.PXR.low_pressure_outlet, destination=m.fs.P2.inlet) m.fs.s10 = Arc(source=m.fs.P2.outlet, destination=m.fs.M1.P2) TransformationFactory("network.expand_arcs").apply_to(m) # scaling m.fs.properties.set_default_scaling('flow_mass_phase_comp', 1, index=('Liq', 'H2O')) m.fs.properties.set_default_scaling('flow_mass_phase_comp', 1e2, index=('Liq', 'NaCl')) iscale.calculate_scaling_factors(m) return m