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 test_heat_exchanger(): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = props.SeawaterParameterBlock() m.fs.unit = HeatExchanger( default={ "hot_side_name": "hot", "cold_side_name": "cold", "hot": { "property_package": m.fs.properties }, "cold": { "property_package": m.fs.properties }, "delta_temperature_callback": delta_temperature_chen_callback, "flow_pattern": HeatExchangerFlowPattern.countercurrent, }) # 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", "TDS")) iscale.set_scaling_factor(m.fs.unit.hot.heat, 1e-3) iscale.set_scaling_factor(m.fs.unit.cold.heat, 1e-3) iscale.set_scaling_factor(m.fs.unit.overall_heat_transfer_coefficient, 1e-3) iscale.set_scaling_factor(m.fs.unit.area, 1) iscale.calculate_scaling_factors(m) # ---specifications--- # state variables m.fs.unit.hot_inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix(1) m.fs.unit.hot_inlet.flow_mass_phase_comp[0, "Liq", "TDS"].fix(0.01) m.fs.unit.hot_inlet.temperature[0].fix(350) m.fs.unit.hot_inlet.pressure[0].fix(2e5) m.fs.unit.cold_inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix(0.5) m.fs.unit.cold_inlet.flow_mass_phase_comp[0, "Liq", "TDS"].fix(0.01) m.fs.unit.cold_inlet.temperature[0].fix(298) m.fs.unit.cold_inlet.pressure[0].fix(2e5) m.fs.unit.area.fix(5) m.fs.unit.overall_heat_transfer_coefficient.fix(1000) # solving assert_units_consistent(m) degrees_of_freedom(m) m.fs.unit.initialize() solver = get_solver() results = solver.solve(m, tee=False) assert_optimal_termination(results) assert pytest.approx(89050.0, rel=1e-4) == value(m.fs.unit.heat_duty[0]) assert pytest.approx(1.0, rel=1e-4) == value( m.fs.unit.hot_outlet.flow_mass_phase_comp[0, "Liq", "H2O"]) assert pytest.approx(0.01, rel=1e-4) == value( m.fs.unit.hot_outlet.flow_mass_phase_comp[0, "Liq", "TDS"]) assert pytest.approx(328.69, rel=1e-4) == value( m.fs.unit.hot_outlet.temperature[0]) assert pytest.approx(2.0e5, rel=1e-4) == value(m.fs.unit.hot_outlet.pressure[0]) assert pytest.approx(0.5, rel=1e-4) == value( m.fs.unit.cold_outlet.flow_mass_phase_comp[0, "Liq", "H2O"]) assert pytest.approx(0.01, rel=1e-4) == value( m.fs.unit.cold_outlet.flow_mass_phase_comp[0, "Liq", "TDS"]) assert pytest.approx(340.78, rel=1e-4) == value( m.fs.unit.cold_outlet.temperature[0]) assert pytest.approx(2.0e5, rel=1e-4) == value(m.fs.unit.cold_outlet.pressure[0]) m.fs.unit.report()
def build_valve_vapor(): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = iapws95.Iapws95ParameterBlock() m.fs.valve = SteamValve(default={"property_package": m.fs.properties}) 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.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_annual_water_production(m.fs.product.properties[0].flow_vol) 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 main(): # Create a Concrete Model as the top level object m = ConcreteModel() # Add a flowsheet object to the model m.fs = FlowsheetBlock(default={"dynamic": False}) # Add property packages to flowsheet library m.fs.thermo_params = thermo_props.HDAParameterBlock() m.fs.reaction_params = reaction_props.HDAReactionParameterBlock( default={"property_package": m.fs.thermo_params}) # Create unit models m.fs.M101 = Mixer( default={ "property_package": m.fs.thermo_params, "inlet_list": ["toluene_feed", "hydrogen_feed", "vapor_recycle"] }) m.fs.H101 = Heater( default={ "property_package": m.fs.thermo_params, "has_pressure_change": False, "has_phase_equilibrium": True }) m.fs.R101 = StoichiometricReactor( default={ "property_package": m.fs.thermo_params, "reaction_package": m.fs.reaction_params, "has_heat_of_reaction": True, "has_heat_transfer": True, "has_pressure_change": False }) m.fs.F101 = Flash( default={ "property_package": m.fs.thermo_params, "has_heat_transfer": True, "has_pressure_change": True }) m.fs.S101 = Splitter( default={ "property_package": m.fs.thermo_params, "ideal_separation": False, "outlet_list": ["purge", "recycle"] }) # This is needed to avoid pressure degeneracy in recylce loop m.fs.C101 = PressureChanger( default={ "property_package": m.fs.thermo_params, "compressor": True, "thermodynamic_assumption": ThermodynamicAssumption.isothermal }) m.fs.F102 = Flash( default={ "property_package": m.fs.thermo_params, "has_heat_transfer": True, "has_pressure_change": True }) m.fs.H101.control_volume.scaling_factor_energy = 1e-3 m.fs.R101.control_volume.scaling_factor_energy = 1e-3 m.fs.F101.control_volume.scaling_factor_energy = 1e-3 m.fs.C101.control_volume.scaling_factor_energy = 1e-3 m.fs.F102.control_volume.scaling_factor_energy = 1e-3 # Connect units m.fs.s03 = Arc(source=m.fs.M101.outlet, destination=m.fs.H101.inlet) m.fs.s04 = Arc(source=m.fs.H101.outlet, destination=m.fs.R101.inlet) m.fs.s05 = Arc(source=m.fs.R101.outlet, destination=m.fs.F101.inlet) m.fs.s06 = Arc(source=m.fs.F101.vap_outlet, destination=m.fs.S101.inlet) m.fs.s08 = Arc(source=m.fs.S101.recycle, destination=m.fs.C101.inlet) m.fs.s09 = Arc(source=m.fs.C101.outlet, destination=m.fs.M101.vapor_recycle) m.fs.s10 = Arc(source=m.fs.F101.liq_outlet, destination=m.fs.F102.inlet) TransformationFactory("network.expand_arcs").apply_to(m) # Set operating conditions m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Vap", "benzene"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Vap", "toluene"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Vap", "hydrogen"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Vap", "methane"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Liq", "benzene"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Liq", "toluene"].fix(0.30) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Liq", "hydrogen"].fix(1e-5) m.fs.M101.toluene_feed.flow_mol_phase_comp[0, "Liq", "methane"].fix(1e-5) m.fs.M101.toluene_feed.temperature.fix(303.2) m.fs.M101.toluene_feed.pressure.fix(350000) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Vap", "benzene"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Vap", "toluene"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Vap", "hydrogen"].fix(0.30) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Vap", "methane"].fix(0.02) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Liq", "benzene"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Liq", "toluene"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Liq", "hydrogen"].fix(1e-5) m.fs.M101.hydrogen_feed.flow_mol_phase_comp[0, "Liq", "methane"].fix(1e-5) m.fs.M101.hydrogen_feed.temperature.fix(303.2) m.fs.M101.hydrogen_feed.pressure.fix(350000) m.fs.H101.outlet.temperature.fix(600) m.fs.R101.conversion = Var(initialize=0.75, bounds=(0, 1)) m.fs.R101.conv_constraint = Constraint( expr=m.fs.R101.conversion * m.fs.R101.inlet.flow_mol_phase_comp[0, "Vap", "toluene"] == ( m.fs.R101.inlet.flow_mol_phase_comp[0, "Vap", "toluene"] - m.fs.R101.outlet.flow_mol_phase_comp[0, "Vap", "toluene"])) m.fs.R101.conversion.fix(0.75) m.fs.R101.heat_duty.fix(0) m.fs.F101.vap_outlet.temperature.fix(325.0) m.fs.F101.deltaP.fix(0) m.fs.S101.split_fraction[0, "purge"].fix(0.2) m.fs.C101.outlet.pressure.fix(350000) m.fs.F102.vap_outlet.temperature.fix(375) m.fs.F102.deltaP.fix(-200000) # Define expressions # Product purity m.fs.purity = Expression( expr=m.fs.F102.vap_outlet.flow_mol_phase_comp[0, "Vap", "benzene"] / (m.fs.F102.vap_outlet.flow_mol_phase_comp[0, "Vap", "benzene"] + m.fs.F102.vap_outlet.flow_mol_phase_comp[0, "Vap", "toluene"])) # Operating cost ($/yr) m.fs.cooling_cost = Expression(expr=0.212e-7 * -m.fs.F101.heat_duty[0] + 0.212e-7 * -m.fs.R101.heat_duty[0]) m.fs.heating_cost = Expression(expr=2.2e-7 * m.fs.H101.heat_duty[0] + 1.9e-7 * m.fs.F102.heat_duty[0]) m.fs.operating_cost = Expression( expr=(3600 * 24 * 365 * (m.fs.heating_cost + m.fs.cooling_cost))) print(degrees_of_freedom(m)) # Initialize Units # Define method for initialising each block def function(unit): unit.initialize(outlvl=1) # Create instance of sequential decomposition tool seq = SequentialDecomposition() seq.options.select_tear_method = "heuristic" seq.options.tear_method = "Wegstein" seq.options.iterLim = 5 # Determine tear stream and calculation order G = seq.create_graph(m) heu_result = seq.tear_set_arcs(G, method="heuristic") order = seq.calculation_order(G) # Display tear stream and calculation order for o in heu_result: print(o.name) for o in order: for oo in o: print(oo.name) # Set guesses for tear stream tear_guesses = { "flow_mol_phase_comp": { (0, "Vap", "benzene"): 1e-5, (0, "Vap", "toluene"): 1e-5, (0, "Vap", "hydrogen"): 0.30, (0, "Vap", "methane"): 0.02, (0, "Liq", "benzene"): 1e-5, (0, "Liq", "toluene"): 0.30, (0, "Liq", "hydrogen"): 1e-5, (0, "Liq", "methane"): 1e-5 }, "temperature": { 0: 303 }, "pressure": { 0: 350000 } } seq.set_guesses_for(m.fs.H101.inlet, tear_guesses) # Run sequential initialisation seq.run(m, function) # # Create a solver solver = SolverFactory('ipopt') solver.options = {'tol': 1e-6} solver.options = {'tol': 1e-6, 'max_iter': 5000} results = solver.solve(m, tee=True) # Print results print("M101 Outlet") m.fs.M101.outlet.display() print("H101 Outlet") m.fs.H101.outlet.display() print("R101 Outlet") m.fs.R101.outlet.display() print("F101") m.fs.F101.liq_outlet.display() m.fs.F101.vap_outlet.display() print("F102") m.fs.F102.liq_outlet.display() m.fs.F102.vap_outlet.display() print("Purge") m.fs.S101.purge.display() print("Purity:", value(m.fs.purity)) # Optimize process m.fs.objective = Objective(sense=minimize, expr=m.fs.operating_cost) # Decision variables m.fs.H101.outlet.temperature.unfix() m.fs.R101.heat_duty.unfix() m.fs.F101.vap_outlet.temperature.unfix() m.fs.F102.vap_outlet.temperature.unfix() m.fs.F102.deltaP.unfix() # Variable bounds m.fs.H101.outlet.temperature[0].setlb(500) m.fs.H101.outlet.temperature[0].setub(600) m.fs.R101.outlet.temperature[0].setlb(600) m.fs.R101.outlet.temperature[0].setub(800) m.fs.F101.vap_outlet.temperature[0].setlb(298.0) m.fs.F101.vap_outlet.temperature[0].setub(450.0) m.fs.F102.vap_outlet.temperature[0].setlb(298.0) m.fs.F102.vap_outlet.temperature[0].setub(450.0) m.fs.F102.vap_outlet.pressure[0].setlb(105000) m.fs.F102.vap_outlet.pressure[0].setub(110000) # Additional Constraints m.fs.overhead_loss = Constraint( expr=m.fs.F101.vap_outlet.flow_mol_phase_comp[0, "Vap", "benzene"] <= 0.20 * m.fs.R101.outlet.flow_mol_phase_comp[0, "Vap", "benzene"]) m.fs.product_flow = Constraint( expr=m.fs.F102.vap_outlet.flow_mol_phase_comp[0, "Vap", "benzene"] >= 0.15) m.fs.product_purity = Constraint(expr=m.fs.purity >= 0.80) # Create a solver solver = SolverFactory('ipopt') solver.options = {'tol': 1e-6} solver.options = {'tol': 1e-6, 'max_iter': 5000} results = solver.solve(m, tee=True) # Print optimization results print() print("Optimal Solution") m.fs.operating_cost.display() m.fs.H101.heat_duty.display() m.fs.R101.heat_duty.display() m.fs.F101.heat_duty.display() m.fs.F102.heat_duty.display() # Print results print("M101 Outlet") m.fs.M101.outlet.display() print("H101 Outlet") m.fs.H101.outlet.display() print("R101 Outlet") m.fs.R101.outlet.display() print("F101") m.fs.F101.liq_outlet.display() m.fs.F101.vap_outlet.display() print("F102") m.fs.F102.liq_outlet.display() m.fs.F102.vap_outlet.display() print("Purge") m.fs.S101.purge.display() print("Recycle") m.fs.S101.recycle.display() print("Purity:", value(m.fs.purity)) # For testing purposes return (m, results)
def build_turbine_dyn(): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": True, "time_set":[0,4]}) m.fs.properties = iapws95.Iapws95ParameterBlock() m.fs.turb = HelmTurbineInletStage(default={"property_package": m.fs.properties}) return m
def main(m): m.fs = FlowsheetBlock(default={ "dynamic": True, "time_set": [0, 3600], "time_units": pyunits.s }) m.fs.gas_props = GasPhaseParameterBlock() m.fs.solid_props = SolidPhaseParameterBlock() m.fs.solid_rxns = HeteroReactionParameterBlock( default={ "solid_property_package": m.fs.solid_props, "gas_property_package": m.fs.gas_props }) m.fs.TGA = FixedBed0D( default={ "energy_balance_type": EnergyBalanceType.none, "gas_property_package": m.fs.gas_props, "solid_property_package": m.fs.solid_props, "reaction_package": m.fs.solid_rxns }) # Discretize time domain m.discretizer = TransformationFactory('dae.finite_difference') m.discretizer.apply_to(m, nfe=100, wrt=m.fs.time, scheme="BACKWARD") # Set reactor design conditions m.fs.TGA.bed_diameter.fix(1) # diameter of the TGA reactor [m] m.fs.TGA.bed_height.fix(1) # height of solids in the TGA reactor [m] # Set initial conditions of the solid phase m.fs.TGA.solids[0].particle_porosity.fix(0.20) m.fs.TGA.solids[0].mass_frac_comp['Fe2O3'].fix(0.45) m.fs.TGA.solids[0].mass_frac_comp['Fe3O4'].fix(0) m.fs.TGA.solids[0].mass_frac_comp['Al2O3'].fix(0.55) m.fs.TGA.solids[0].temperature.fix(1273.15) # Set conditions of the gas phase (this is all fixed as gas side assumption # is excess gas flowrate which means all state variables remain unchanged) for t in m.fs.time: m.fs.TGA.gas[t].temperature.fix(1273.15) m.fs.TGA.gas[t].pressure.fix(1.01325) # 1atm m.fs.TGA.gas[t].mole_frac_comp['CO2'].fix(0.4) m.fs.TGA.gas[t].mole_frac_comp['H2O'].fix(0.5) m.fs.TGA.gas[t].mole_frac_comp['CH4'].fix(0.1) # Solver options optarg = { "bound_push": 1e-8, 'halt_on_ampl_error': 'yes', 'linear_solver': 'ma27' } t_start = time.time() # Run start time m.fs.TGA.initialize() t_initialize = time.time() # Initialization time solver = get_solver('ipopt', optarg) # create solver initialize_by_time_element(m.fs, m.fs.time, solver=solver) solver.solve(m, tee=True) t_simulation = time.time() # Simulation time print("\n") print("----------------------------------------------------------") print('Total initialization time: ', value(t_initialize - t_start), " s") print("----------------------------------------------------------") print("\n") print("----------------------------------------------------------") print('Total simulation time: ', value(t_simulation - t_start), " s") print("----------------------------------------------------------") return m
def test_bad_option(): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) with pytest.raises(KeyError): m.fs.unit = HeatExchanger(default={"I'm a bad option": "hot"})
def test_same_name(): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) with pytest.raises(NameError): m.fs.unit = HeatExchanger(default={"cold_side_name": "shell"})
def test_Pdrop_fixed_per_unit_length(self): """Testing 0D-RO with PressureChangeType.fixed_per_unit_length option.""" m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = props.NaClParameterBlock() m.fs.unit = ReverseOsmosis0D( default={ "property_package": m.fs.properties, "has_pressure_change": True, "concentration_polarization_type": ConcentrationPolarizationType.calculated, "mass_transfer_coefficient": MassTransferCoefficient.calculated, "pressure_change_type": PressureChangeType.fixed_per_unit_length, }) # fully specify system feed_flow_mass = 1 feed_mass_frac_NaCl = 0.035 feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl feed_pressure = 50e5 feed_temperature = 273.15 + 25 membrane_area = 50 length = 20 A = 4.2e-12 B = 3.5e-8 pressure_atmospheric = 101325 membrane_pressure_drop = 3e5 m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( feed_flow_mass * feed_mass_frac_NaCl) m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( feed_flow_mass * feed_mass_frac_H2O) m.fs.unit.inlet.pressure[0].fix(feed_pressure) m.fs.unit.inlet.temperature[0].fix(feed_temperature) m.fs.unit.area.fix(membrane_area) m.fs.unit.A_comp.fix(A) m.fs.unit.B_comp.fix(B) m.fs.unit.permeate.pressure[0].fix(pressure_atmospheric) m.fs.unit.channel_height.fix(0.002) m.fs.unit.spacer_porosity.fix(0.75) m.fs.unit.length.fix(length) m.fs.unit.dP_dx.fix(-membrane_pressure_drop / length) # test statistics assert number_variables(m) == 142 assert number_total_constraints(m) == 112 assert number_unused_variables(m) == 0 # test degrees of freedom assert degrees_of_freedom(m) == 0 # test 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")) calculate_scaling_factors(m) # check that all variables have scaling factors. # TODO: see aforementioned TODO on revisiting scaling and associated testing for property models. unscaled_var_list = list( unscaled_variables_generator(m.fs.unit, include_fixed=True)) assert len(unscaled_var_list) == 0 # test initialization initialization_tester(m, fail_on_warning=True) # test variable scaling badly_scaled_var_lst = list(badly_scaled_var_generator(m)) assert badly_scaled_var_lst == [] # test solve results = solver.solve(m, tee=True) # Check for optimal solution assert_optimal_termination(results) # test solution assert pytest.approx(-3.000e5, rel=1e-3) == value(m.fs.unit.deltaP[0]) assert pytest.approx(4.562e-3, rel=1e-3) == value( m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "H2O"]) assert pytest.approx(1.593e-6, rel=1e-3) == value( m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "NaCl"]) assert pytest.approx(0.2281, rel=1e-3) == value( m.fs.unit.mixed_permeate[0].flow_mass_phase_comp["Liq", "H2O"]) assert pytest.approx(7.963e-5, rel=1e-3) == value( m.fs.unit.mixed_permeate[0].flow_mass_phase_comp["Liq", "NaCl"]) assert pytest.approx(41.96, rel=1e-3) == value( m.fs.unit.feed_side.properties_interface[ 0, 0.0].conc_mass_phase_comp["Liq", "NaCl"]) assert pytest.approx(46.57, rel=1e-3) == value( m.fs.unit.feed_side.properties_out[0].conc_mass_phase_comp["Liq", "NaCl"]) assert pytest.approx(49.94, rel=1e-3) == value( m.fs.unit.feed_side.properties_interface[ 0, 1.0].conc_mass_phase_comp["Liq", "NaCl"])
def test_initialize_by_time_element(): horizon = 6 time_set = [0, horizon] ntfe = 60 # For a finite element every six seconds ntcp = 2 m = ConcreteModel(name='CSTR model for testing') 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={"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}) # Time discretization 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: 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) # Fix inlet conditions # This is a huge hack because I didn't know that the proper way to # have multiple inlets to a CSTR was to use a mixer. # I 'combine' both my inlet streams before sending them to the CSTR. for t, j in m.fs.time*m.fs.properties.component_list: if t <= 2: if j == 'E': m.fs.cstr.inlet.conc_mol[t, j].fix(11.91*0.1/2.2) elif j == 'S': m.fs.cstr.inlet.conc_mol[t, j].fix(12.92*2.1/2.2) elif j != 'Solvent': m.fs.cstr.inlet.conc_mol[t, j].fix(0) elif t <= 4: if j == 'E': m.fs.cstr.inlet.conc_mol[t, j].fix(5.95*0.1/2.2) elif j == 'S': m.fs.cstr.inlet.conc_mol[t, j].fix(12.92*2.1/2.2) elif j != 'Solvent': m.fs.cstr.inlet.conc_mol[t, j].fix(0) else: if j == 'E': m.fs.cstr.inlet.conc_mol[t, j].fix(8.95*0.1/2.2) elif j == 'S': m.fs.cstr.inlet.conc_mol[t, j].fix(16.75*2.1/2.2) elif j != 'Solvent': m.fs.cstr.inlet.conc_mol[t, j].fix(0) m.fs.cstr.inlet.conc_mol[:, 'Solvent'].fix(1.) m.fs.cstr.inlet.flow_vol.fix(2.2) m.fs.cstr.inlet.temperature.fix(300) # Fix outlet conditions m.fs.cstr.outlet.flow_vol.fix(2.2) m.fs.cstr.outlet.temperature[m.fs.time.first()].fix(300) assert degrees_of_freedom(m) == 0 initialize_by_time_element(m.fs, m.fs.time, solver=solver) assert degrees_of_freedom(m) == 0 # Assert that the result looks how we expect assert m.fs.cstr.outlet.conc_mol[0, 'S'].value == 0 assert abs(m.fs.cstr.outlet.conc_mol[2, 'S'].value - 11.389) < 1e-2 assert abs(m.fs.cstr.outlet.conc_mol[4, 'P'].value - 0.2191) < 1e-3 assert abs(m.fs.cstr.outlet.conc_mol[6, 'E'].value - 0.0327) < 1e-3 assert abs(m.fs.cstr.outlet.temperature[6].value - 289.7) < 1 # Assert that model is still fixed and deactivated as expected assert ( m.fs.cstr.control_volume.material_holdup[m.fs.time.first(), 'aq', 'S'].fixed) for t in m.fs.time: if t != m.fs.time.first(): assert (not m.fs.cstr.control_volume.material_holdup[t, 'aq', 'S'].fixed) assert not m.fs.cstr.outlet.temperature[t].fixed assert ( m.fs.cstr.control_volume.material_holdup_calculation[t, 'aq', 'C'].active) assert m.fs.cstr.control_volume.properties_out[t].active assert not m.fs.cstr.outlet.conc_mol[t, 'S'].fixed assert m.fs.cstr.inlet.conc_mol[t, 'S'].fixed # Assert that constraints are feasible after initialization for con in m.fs.component_data_objects(Constraint, active=True): assert value(con.body) - value(con.upper) < 1e-5 assert value(con.lower) - value(con.body) < 1e-5 results = solver.solve(m.fs) assert results.solver.termination_condition == TerminationCondition.optimal
def test_CP_calculation_with_kf_fixed(self): """Testing 0D-RO with ConcentrationPolarizationType.calculated option enabled. This option makes use of an alternative constraint for the feed-side, membrane-interface concentration. Additionally, two more variables are created when this option is enabled: Kf - feed-channel mass transfer coefficients at the channel inlet and outlet. """ m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = props.NaClParameterBlock() m.fs.unit = ReverseOsmosis0D( default={ "property_package": m.fs.properties, "has_pressure_change": True, "concentration_polarization_type": ConcentrationPolarizationType.calculated, "mass_transfer_coefficient": MassTransferCoefficient.fixed, }) # fully specify system feed_flow_mass = 1 feed_mass_frac_NaCl = 0.035 feed_pressure = 50e5 feed_temperature = 273.15 + 25 membrane_pressure_drop = 3e5 membrane_area = 50 A = 4.2e-12 B = 3.5e-8 pressure_atmospheric = 101325 kf = 2e-5 feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( feed_flow_mass * feed_mass_frac_NaCl) m.fs.unit.inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( feed_flow_mass * feed_mass_frac_H2O) m.fs.unit.inlet.pressure[0].fix(feed_pressure) m.fs.unit.inlet.temperature[0].fix(feed_temperature) m.fs.unit.deltaP.fix(-membrane_pressure_drop) m.fs.unit.area.fix(membrane_area) m.fs.unit.A_comp.fix(A) m.fs.unit.B_comp.fix(B) m.fs.unit.permeate.pressure[0].fix(pressure_atmospheric) m.fs.unit.Kf[0, 0.0, "NaCl"].fix(kf) m.fs.unit.Kf[0, 1.0, "NaCl"].fix(kf) # test statistics assert number_variables(m) == 125 assert number_total_constraints(m) == 96 assert number_unused_variables( m) == 7 # vars from property package parameters # test degrees of freedom assert degrees_of_freedom(m) == 0 # test 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")) calculate_scaling_factors(m) # check that all variables have scaling factors. # TODO: Setting the "include_fixed" arg as True reveals # unscaled vars that weren't being accounted for previously. However, calling the whole block (i.e., # m) shows that several NaCl property parameters are unscaled. For now, we are just interested in ensuring # unit variables are scaled (hence, calling m.fs.unit) but might need to revisit scaling and associated # testing for property models. unscaled_var_list = list( unscaled_variables_generator(m.fs.unit, include_fixed=True)) assert len(unscaled_var_list) == 0 # # test initialization initialization_tester(m, fail_on_warning=True) # test variable scaling badly_scaled_var_lst = list(badly_scaled_var_generator(m)) assert badly_scaled_var_lst == [] # test solve results = solver.solve(m) # Check for optimal solution assert_optimal_termination(results) # test solution assert pytest.approx(3.815e-3, rel=1e-3) == value( m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "H2O"]) assert pytest.approx(1.668e-6, rel=1e-3) == value( m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "NaCl"]) assert pytest.approx(0.1908, rel=1e-3) == value( m.fs.unit.mixed_permeate[0].flow_mass_phase_comp["Liq", "H2O"]) assert pytest.approx(8.337e-5, rel=1e-3) == value( m.fs.unit.mixed_permeate[0].flow_mass_phase_comp["Liq", "NaCl"]) assert pytest.approx(46.07, rel=1e-3) == value( m.fs.unit.feed_side.properties_interface[ 0, 0.0].conc_mass_phase_comp["Liq", "NaCl"]) assert pytest.approx(44.34, rel=1e-3) == value( m.fs.unit.feed_side.properties_out[0].conc_mass_phase_comp["Liq", "NaCl"]) assert pytest.approx(50.20, rel=1e-3) == value( m.fs.unit.feed_side.properties_interface[ 0, 1.0].conc_mass_phase_comp["Liq", "NaCl"])
def test_costing_distillation_solve(): m = pyo.ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.get_costing() m.fs.costing.CE_index = 550 # create a unit model and variables m.fs.unit = pyo.Block() m.fs.unit.heat_duty = pyo.Var(initialize=1e6, units=pyo.units.BTU / pyo.units.hr) m.fs.unit.pressure = pyo.Var(initialize=1e5, units=pyo.units.psi) m.fs.unit.diameter = pyo.Var(initialize=10, domain=pyo.NonNegativeReals, units=pyo.units.foot) m.fs.unit.length = pyo.Var(initialize=10, domain=pyo.NonNegativeReals, units=pyo.units.foot) # create costing block m.fs.unit.costing = pyo.Block() cs.vessel_costing(m.fs.unit.costing, alignment='vertical', weight_limit='option2', L_D_range='option2', PL=True, plates=True, number_tray=100, ref_parameter_diameter=m.fs.unit.diameter, ref_parameter_length=m.fs.unit.length) # pressure design and shell thickness from Example 22.13 Product and # Process Design Principless m.fs.unit.heat_duty.fix(18390000) # Btu/hr m.fs.unit.pressure.fix(123 + 14.6959) # psia # pressure design minimum thickness tp = 0.582 in # vessel is vertical + quite tall the tower is subject to wind load, # and earthquake. Assume wall thickness of 1.25 in. # The additional wall thickness at the bottom of the tower is 0.889 in # average thickness is 1.027, plus corrosion allowance of 1/8 # 1.152 in, therefore steel plate thickness is 1.250 (ts) m.fs.unit.costing.shell_thickness.set_value(1.250) # inches m.fs.unit.diameter.fix(10) # ft m.fs.unit.length.fix(212) # ft m.fs.unit.costing.number_trays.set_value(100) assert degrees_of_freedom(m) == 0 # Check unit config arguments assert isinstance(m.fs.unit.costing.purchase_cost, pyo.Var) assert isinstance(m.fs.unit.costing.base_cost_per_unit, pyo.Var) results = solver.solve(m, tee=False) # Check for optimal solution assert results.solver.termination_condition == \ pyo.TerminationCondition.optimal assert results.solver.status == pyo.SolverStatus.ok assert (pytest.approx(pyo.value(m.fs.unit.costing.base_cost), abs=1e-2) == 636959.6929) # Example 22.13 Ref Book assert (pytest.approx(pyo.value(m.fs.unit.costing.base_cost_platf_ladders), abs=1e-2) == 97542.9005) # Example 22.13 Ref Book assert (pytest.approx(pyo.value(m.fs.unit.costing.purchase_cost_trays), abs=1e-2) == 293006.086) # Example 22.13 Ref Book assert (pytest.approx(pyo.value(m.fs.unit.costing.purchase_cost), abs=1e-2) == 1100958.9396) # Example 22.13 Ref Book
def main(): # --------------------------------------------------------------------- # Build model # Create a concrete model m = ConcreteModel() # Create a steady-state flowsheet m.fs = FlowsheetBlock(default={"dynamic": False}) # Set up thermo-physical and reaction properties m.fs.gas_properties = GasPhaseThermoParameterBlock() m.fs.solid_properties = SolidPhaseThermoParameterBlock() m.fs.hetero_reactions = HeteroReactionParameterBlock( default={ "solid_property_package": m.fs.solid_properties, "gas_property_package": m.fs.gas_properties }) # Build the BFB in the flowsheet m.fs.BFB = BubblingFluidizedBed( default={ "flow_type": "co_current", "finite_elements": 5, "transformation_method": "dae.collocation", "gas_phase_config": { "property_package": m.fs.gas_properties }, "solid_phase_config": { "property_package": m.fs.solid_properties, "reaction_package": m.fs.hetero_reactions } }) # --------------------------------------------------------------------- # Set design and operating variables of the BFB model # Fix design variables m.fs.BFB.number_orifice.fix(2500) # [-] m.fs.BFB.bed_diameter.fix(6.5) # m m.fs.BFB.bed_height.fix(5) # m # Fix inlet port variables for gas and solid m.fs.BFB.gas_inlet.flow_mol[0].fix(272.81) # mol/s m.fs.BFB.gas_inlet.temperature[0].fix(373) # K m.fs.BFB.gas_inlet.pressure[0].fix(1.86) # bar m.fs.BFB.gas_inlet.mole_frac_comp[0, "CO2"].fix(0.4772) m.fs.BFB.gas_inlet.mole_frac_comp[0, "H2O"].fix(0.0646) m.fs.BFB.gas_inlet.mole_frac_comp[0, "CH4"].fix(0.4582) m.fs.BFB.solid_inlet.flow_mass[0].fix(1230) # kg/s m.fs.BFB.solid_inlet.temperature[0].fix(1186) # K m.fs.BFB.solid_inlet.mass_frac_comp[0, "Fe2O3"].fix(0.45) m.fs.BFB.solid_inlet.mass_frac_comp[0, "Fe3O4"].fix(1e-9) m.fs.BFB.solid_inlet.mass_frac_comp[0, "Al2O3"].fix(0.55) # --------------------------------------------------------------------- # Initialize reactor t_start = time.time() # Run start time # State arguments for initializing property state blocks # Bubble and gas_emulsion temperatures are initialized at solid # temperature because thermal mass of solid >> thermal mass of gas blk = m.fs.BFB gas_phase_state_args = { 'flow_mol': blk.gas_inlet.flow_mol[0].value, 'temperature': blk.solid_inlet.temperature[0].value, 'pressure': blk.gas_inlet.pressure[0].value, 'mole_frac': { 'CH4': blk.gas_inlet.mole_frac_comp[0, 'CH4'].value, 'CO2': blk.gas_inlet.mole_frac_comp[0, 'CO2'].value, 'H2O': blk.gas_inlet.mole_frac_comp[0, 'H2O'].value } } solid_phase_state_args = { 'flow_mass': blk.solid_inlet.flow_mass[0].value, 'temperature': blk.solid_inlet.temperature[0].value, 'mass_frac': { 'Fe2O3': blk.solid_inlet.mass_frac_comp[0, 'Fe2O3'].value, 'Fe3O4': blk.solid_inlet.mass_frac_comp[0, 'Fe3O4'].value, 'Al2O3': blk.solid_inlet.mass_frac_comp[0, 'Al2O3'].value } } m.fs.BFB.initialize(outlvl=idaeslog.INFO, gas_phase_state_args=gas_phase_state_args, solid_phase_state_args=solid_phase_state_args) t_initialize = time.time() # Initialization time # --------------------------------------------------------------------- # Final solve # Create a solver solver = SolverFactory('ipopt') solver.solve(m.fs.BFB, tee=True) t_simulation = time.time() # Simulation time print("\n") print("----------------------------------------------------------") print('Total initialization time: ', value(t_initialize - t_start), " s") print("----------------------------------------------------------") print("\n") print("----------------------------------------------------------") print('Total simulation time: ', value(t_simulation - t_start), " s") print("----------------------------------------------------------") return m
def test_T368_P1_x5(self): m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.props = BT_PR.BTParameterBlock( default={'valid_phase': ('Vap', 'Liq')}) m.fs.state = m.fs.props.build_state_block( default={'defined_state': True}) m.fs.state.flow_mol.fix(100) m.fs.state.mole_frac_comp["benzene"].fix(0.5) m.fs.state.mole_frac_comp["toluene"].fix(0.5) m.fs.state.temperature.fix(368) m.fs.state.pressure.fix(1e5) # Trigger build of enthalpy and entropy m.fs.state.enth_mol_phase m.fs.state.entr_mol_phase m.fs.state.initialize() solver.solve(m) assert pytest.approx(value(m.fs.state._teq), 1e-5) == 368 assert 0.003504 == pytest.approx( value(m.fs.state.compress_fact_phase["Liq"]), 1e-5) assert 0.97 == pytest.approx( value(m.fs.state.compress_fact_phase["Vap"]), 1e-5) assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "benzene"]), 1e-5) == 1.492049 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "toluene"]), 1e-5) == 0.621563 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "benzene"]), 1e-5) == 0.97469 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "toluene"]), 1e-5) == 0.964642 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "benzene"]), 1e-5) == 0.4012128 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "toluene"]), 1e-5) == 0.5987872 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "benzene"]), 1e-5) == 0.6141738 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "toluene"]), 1e-5) == 0.3858262 assert pytest.approx( value(m.fs.state.enth_mol_phase["Liq"]), 1e-5) == 38235.1 assert pytest.approx( value(m.fs.state.enth_mol_phase["Vap"]), 1e-5) == 77155.4 assert pytest.approx( value(m.fs.state.entr_mol_phase["Liq"]), 1e-5) == -364.856 assert pytest.approx( value(m.fs.state.entr_mol_phase["Vap"]), 1e-5) == -267.892
def btx(self): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) # As we lack other example prop packs with units, take the generic # BT-PR package and change the base units configuration2 = { # Specifying components "components": { 'benzene': { "type": Component, "enth_mol_ig_comp": RPP, "entr_mol_ig_comp": RPP, "pressure_sat_comp": RPP, "phase_equilibrium_form": { ("Vap", "Liq"): log_fugacity }, "parameter_data": { "mw": (78.1136E-3, pyunits.kg / pyunits.mol), # [1] "pressure_crit": (48.9e5, pyunits.Pa), # [1] "temperature_crit": (562.2, pyunits.K), # [1] "omega": 0.212, # [1] "cp_mol_ig_comp_coeff": { 'A': (-3.392E1, pyunits.J / pyunits.mol / pyunits.K), # [1] 'B': (4.739E-1, pyunits.J / pyunits.mol / pyunits.K**2), 'C': (-3.017E-4, pyunits.J / pyunits.mol / pyunits.K**3), 'D': (7.130E-8, pyunits.J / pyunits.mol / pyunits.K**4) }, "enth_mol_form_vap_comp_ref": (82.9e3, pyunits.J / pyunits.mol), # [3] "entr_mol_form_vap_comp_ref": (-269, pyunits.J / pyunits.mol / pyunits.K), # [3] "pressure_sat_comp_coeff": { 'A': (-6.98273, None), # [1] 'B': (1.33213, None), 'C': (-2.62863, None), 'D': (-3.33399, None) } } }, 'toluene': { "type": Component, "enth_mol_ig_comp": RPP, "entr_mol_ig_comp": RPP, "pressure_sat_comp": RPP, "phase_equilibrium_form": { ("Vap", "Liq"): log_fugacity }, "parameter_data": { "mw": (92.1405E-3, pyunits.kg / pyunits.mol), # [1] "pressure_crit": (41e5, pyunits.Pa), # [1] "temperature_crit": (591.8, pyunits.K), # [1] "omega": 0.263, # [1] "cp_mol_ig_comp_coeff": { 'A': (-2.435E1, pyunits.J / pyunits.mol / pyunits.K), # [1] 'B': (5.125E-1, pyunits.J / pyunits.mol / pyunits.K**2), 'C': (-2.765E-4, pyunits.J / pyunits.mol / pyunits.K**3), 'D': (4.911E-8, pyunits.J / pyunits.mol / pyunits.K**4) }, "enth_mol_form_vap_comp_ref": (50.1e3, pyunits.J / pyunits.mol), # [3] "entr_mol_form_vap_comp_ref": (-321, pyunits.J / pyunits.mol / pyunits.K), # [3] "pressure_sat_comp_coeff": { 'A': (-7.28607, None), # [1] 'B': (1.38091, None), 'C': (-2.83433, None), 'D': (-2.79168, None) } } } }, # Specifying phases "phases": { 'Liq': { "type": LiquidPhase, "equation_of_state": Cubic, "equation_of_state_options": { "type": CubicType.PR } }, 'Vap': { "type": VaporPhase, "equation_of_state": Cubic, "equation_of_state_options": { "type": CubicType.PR } } }, # Set base units of measurement "base_units": { "time": pyunits.s, "length": pyunits.m, "mass": pyunits.t, "amount": pyunits.mol, "temperature": pyunits.degR }, # Specifying state definition "state_definition": FTPx, "state_bounds": { "flow_mol": (0, 100, 1000, pyunits.mol / pyunits.s), "temperature": (273.15, 300, 500, pyunits.K), "pressure": (5e4, 1e5, 1e6, pyunits.Pa) }, "pressure_ref": (101325, pyunits.Pa), "temperature_ref": (298.15, pyunits.K), # Defining phase equilibria "phases_in_equilibrium": [("Vap", "Liq")], "phase_equilibrium_state": { ("Vap", "Liq"): smooth_VLE }, "bubble_dew_method": LogBubbleDew, "parameter_data": { "PR_kappa": { ("benzene", "benzene"): 0.000, ("benzene", "toluene"): 0.000, ("toluene", "benzene"): 0.000, ("toluene", "toluene"): 0.000 } } } m.fs.properties = GenericParameterBlock(default=configuration) m.fs.properties2 = GenericParameterBlock(default=configuration2) m.fs.unit = HeatExchanger( default={ "shell": { "property_package": m.fs.properties }, "tube": { "property_package": m.fs.properties2 }, "flow_pattern": HeatExchangerFlowPattern.cocurrent }) m.fs.unit.inlet_1.flow_mol[0].fix(5) # mol/s m.fs.unit.inlet_1.temperature[0].fix(365) # K m.fs.unit.inlet_1.pressure[0].fix(101325) # Pa m.fs.unit.inlet_1.mole_frac_comp[0, "benzene"].fix(0.5) m.fs.unit.inlet_1.mole_frac_comp[0, "toluene"].fix(0.5) m.fs.unit.inlet_2.flow_mol[0].fix(1) # mol/s m.fs.unit.inlet_2.temperature[0].fix(540) # degR m.fs.unit.inlet_2.pressure[0].fix(101.325) # kPa m.fs.unit.inlet_2.mole_frac_comp[0, "benzene"].fix(0.5) m.fs.unit.inlet_2.mole_frac_comp[0, "toluene"].fix(0.5) m.fs.unit.area.fix(1) m.fs.unit.overall_heat_transfer_coefficient.fix(100) m.fs.unit.side_2.scaling_factor_pressure = 1 return m
def test_T376_P1_x2(self): m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.props = BT_PR.BTParameterBlock( default={'valid_phase': ('Vap', 'Liq')}) m.fs.state = m.fs.props.build_state_block( default={'defined_state': True}) m.fs.state.flow_mol.fix(100) m.fs.state.mole_frac_comp["benzene"].fix(0.2) m.fs.state.mole_frac_comp["toluene"].fix(0.8) m.fs.state.temperature.fix(376) m.fs.state.pressure.fix(1e5) # Trigger build of enthalpy and entropy m.fs.state.enth_mol_phase m.fs.state.entr_mol_phase m.fs.state.initialize() solver.solve(m) assert pytest.approx(value(m.fs.state._teq), 1e-5) == 376 assert 0.00361333 == pytest.approx( value(m.fs.state.compress_fact_phase["Liq"]), 1e-5) assert 0.968749 == pytest.approx( value(m.fs.state.compress_fact_phase["Vap"]), 1e-5) assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "benzene"]), 1e-5) == 1.8394188 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "toluene"]), 1e-5) == 0.7871415 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "benzene"]), 1e-5) == 0.9763608 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "toluene"]), 1e-5) == 0.9663611 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "benzene"]), 1e-5) == 0.17342 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "toluene"]), 1e-5) == 0.82658 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "benzene"]), 1e-5) == 0.3267155 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "toluene"]), 1e-5) == 0.6732845 assert pytest.approx( value(m.fs.state.enth_mol_phase["Liq"]), 1e-5) == 31535.8 assert pytest.approx( value(m.fs.state.enth_mol_phase["Vap"]), 1e-5) == 69175.3 assert pytest.approx( value(m.fs.state.entr_mol_phase["Liq"]), 1e-5) == -372.869 assert pytest.approx( value(m.fs.state.entr_mol_phase["Vap"]), 1e-5) == -278.766
def create_model(steady_state=True, time_set=[0, 3], time_units=pyo.units.s, nfe=5, calc_integ=True, form=PIDForm.standard): """ Create a test model and solver Args: steady_state (bool): If True, create a steady state model, otherwise create a dynamic model time_set (list): The begining and end point of the time domain time_units (Pyomo Unit object): Units of time domain nfe (int): Number of finite elements argument for the DAE transformation. calc_integ (bool): If True, calculate in the initial condition for the integral term, else use a fixed variable (fs.ctrl.err_i0), False is the better option if you have a value from a previous time period form: whether the equations are written in the standard or velocity form Returns (tuple): (ConcreteModel, Solver) """ if steady_state: fs_cfg = {"dynamic": False} model_name = "Steam Tank, Steady State" else: fs_cfg = { "dynamic": True, "time_set": time_set, "time_units": time_units } model_name = "Steam Tank, Dynamic" m = pyo.ConcreteModel(name=model_name) m.fs = FlowsheetBlock(default=fs_cfg) # Create a property parameter block m.fs.prop_water = iapws95.Iapws95ParameterBlock( default={"phase_presentation": iapws95.PhaseType.LG}) # Create the valve and tank models m.fs.valve_1 = SteamValve( default={ "dynamic": False, "has_holdup": False, "material_balance_type": MaterialBalanceType.componentTotal, "property_package": m.fs.prop_water }) m.fs.tank = Heater( default={ "has_holdup": True, "material_balance_type": MaterialBalanceType.componentTotal, "property_package": m.fs.prop_water }) m.fs.valve_2 = SteamValve( default={ "dynamic": False, "has_holdup": False, "material_balance_type": MaterialBalanceType.componentTotal, "property_package": m.fs.prop_water }) # Connect the models m.fs.v1_to_t = Arc(source=m.fs.valve_1.outlet, destination=m.fs.tank.inlet) m.fs.t_to_v2 = Arc(source=m.fs.tank.outlet, destination=m.fs.valve_2.inlet) # The control volume block doesn't assume the two phases are in equilibrium # by default, so I'll make that assumption here, I don't actually expect # liquid to form but who knows. The phase_fraction in the control volume is # volumetric phase fraction hence the densities. @m.fs.tank.Constraint(m.fs.time) def vol_frac_vap(b, t): return (b.control_volume.properties_out[t].phase_frac["Vap"] * b.control_volume.properties_out[t].dens_mol / b.control_volume.properties_out[t].dens_mol_phase["Vap"]) == ( b.control_volume.phase_fraction[t, "Vap"]) # Add the stream constraints and do the DAE transformation pyo.TransformationFactory('network.expand_arcs').apply_to(m.fs) if not steady_state: pyo.TransformationFactory('dae.finite_difference').apply_to( m.fs, nfe=nfe, wrt=m.fs.time, scheme='BACKWARD') # Fix the derivative variables to zero at time 0 (steady state assumption) m.fs.fix_initial_conditions() # A tank pressure reference that's directly time-indexed m.fs.tank_pressure = pyo.Reference( m.fs.tank.control_volume.properties_out[:].pressure) # Add a controller m.fs.ctrl = PIDBlock( default={ "pv": m.fs.tank_pressure, "output": m.fs.valve_1.valve_opening, "upper": 1.0, "lower": 0.0, "calculate_initial_integral": calc_integ, "pid_form": form }) m.fs.ctrl.deactivate() # Don't want controller turned on by default # Fix the input variables m.fs.valve_1.inlet.enth_mol.fix(50000) m.fs.valve_1.inlet.pressure.fix(5e5) m.fs.valve_2.outlet.pressure.fix(101325) m.fs.valve_1.Cv.fix(0.001) m.fs.valve_2.Cv.fix(0.001) m.fs.valve_1.valve_opening.fix(1) m.fs.valve_2.valve_opening.fix(1) m.fs.tank.heat_duty.fix(0) m.fs.tank.control_volume.volume.fix(2.0) m.fs.ctrl.gain.fix(1e-6) m.fs.ctrl.time_i.fix(0.1) m.fs.ctrl.time_d.fix(0.1) m.fs.ctrl.setpoint.fix(3e5) # Initialize the model solver = pyo.SolverFactory("ipopt") solver.options = {'tol': 1e-6, 'linear_solver': "ma27", 'max_iter': 100} for t in m.fs.time: m.fs.valve_1.inlet.flow_mol = 100 # initial guess on flow # simple initialize m.fs.valve_1.initialize(outlvl=1) _set_port(m.fs.tank.inlet, m.fs.valve_1.outlet) m.fs.tank.initialize(outlvl=1) _set_port(m.fs.valve_2.inlet, m.fs.tank.outlet) m.fs.valve_2.initialize(outlvl=1) solver.solve(m, tee=True) # Return the model and solver return m, solver
def test_seawater_data(): m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.properties = DSPMDEParameterBlock( default={ "solute_list": ["Ca_2+", "SO4_2-", "Na_+", "Cl_-", "Mg_2+"], "diffusivity_data": { ("Liq", "Ca_2+"): 0.792e-9, ("Liq", "SO4_2-"): 1.06e-9, ("Liq", "Na_+"): 1.33e-9, ("Liq", "Cl_-"): 2.03e-9, ("Liq", "Mg_2+"): 0.706e-9 }, "mw_data": { "H2O": 18e-3, "Na_+": 23e-3, "Ca_2+": 40e-3, "Mg_2+": 24e-3, "Cl_-": 35e-3, "SO4_2-": 96e-3 }, "stokes_radius_data": { "Na_+": 0.184e-9, "Ca_2+": 0.309e-9, "Mg_2+": 0.347e-9, "Cl_-": 0.121e-9, "SO4_2-": 0.230e-9 }, "charge": { "Na_+": 1, "Ca_2+": 2, "Mg_2+": 2, "Cl_-": -1, "SO4_2-": -2 }, }) m.fs.stream = stream = m.fs.properties.build_state_block( [0], default={'defined_state': True}) mass_flow_in = 1 * pyunits.kg / pyunits.s feed_mass_frac = { 'Na_+': 11122e-6, 'Ca_2+': 382e-6, 'Mg_2+': 1394e-6, 'SO4_2-': 2136e-6, 'Cl_-': 20300e-6 } for ion, x in feed_mass_frac.items(): mol_comp_flow = x * pyunits.kg / pyunits.kg * mass_flow_in / stream[ 0].mw_comp[ion] stream[0].flow_mol_phase_comp['Liq', ion].fix(mol_comp_flow) H2O_mass_frac = 1 - sum(x for x in feed_mass_frac.values()) H2O_mol_comp_flow = H2O_mass_frac * pyunits.kg / pyunits.kg * mass_flow_in / stream[ 0].mw_comp['H2O'] stream[0].flow_mol_phase_comp['Liq', 'H2O'].fix(H2O_mol_comp_flow) stream[0].temperature.fix(298.15) stream[0].pressure.fix(101325) stream[0].assert_electroneutrality(tol=1e-2) metadata = m.fs.properties.get_metadata().properties for v_name in metadata: getattr(stream[0], v_name) assert stream[0].is_property_constructed('conc_mol_phase_comp') assert_units_consistent(m) check_dof(m, fail_flag=True) m.fs.properties.set_default_scaling('flow_mol_phase_comp', 1, index=('Liq', 'H2O')) m.fs.properties.set_default_scaling('flow_mol_phase_comp', 1, index=('Liq', 'Na_+')) m.fs.properties.set_default_scaling('flow_mol_phase_comp', 1, index=('Liq', 'Cl_-')) m.fs.properties.set_default_scaling('flow_mol_phase_comp', 1e1, index=('Liq', 'Ca_2+')) m.fs.properties.set_default_scaling('flow_mol_phase_comp', 1e1, index=('Liq', 'SO4_2-')) m.fs.properties.set_default_scaling('flow_mol_phase_comp', 1, index=('Liq', 'Mg_2+')) calculate_scaling_factors(m) # check if any variables are badly scaled badly_scaled_var_list = list(badly_scaled_var_generator(m)) assert len(badly_scaled_var_list) == 0 stream.initialize() # check if any variables are badly scaled badly_scaled_var_list = list(badly_scaled_var_generator(m)) assert len(badly_scaled_var_list) == 0 results = solver.solve(m) assert_optimal_termination(results) assert value(stream[0].flow_vol_phase['Liq']) == pytest.approx(0.001, rel=1e-3) assert value(stream[0].flow_mol_phase_comp['Liq', 'H2O']) == pytest.approx( 53.59256, rel=1e-3) assert value(stream[0].flow_mol_phase_comp['Liq', 'Na_+']) == pytest.approx( 0.4836, rel=1e-3) assert value(stream[0].flow_mol_phase_comp['Liq', 'Ca_2+']) == pytest.approx( 0.00955, rel=1e-3) assert value(stream[0].flow_mol_phase_comp['Liq', 'Mg_2+']) == pytest.approx( 0.05808, rel=1e-3) assert value(stream[0].flow_mol_phase_comp['Liq', 'Cl_-']) == pytest.approx( 0.58, rel=1e-3) assert value(stream[0].flow_mol_phase_comp['Liq', 'SO4_2-']) == pytest.approx( 0.02225, rel=1e-3) assert value(stream[0].dens_mass_phase['Liq']) == pytest.approx(1000, rel=1e-3) assert value(stream[0].pressure_osm) == pytest.approx(28.593e5, rel=1e-3) assert value(stream[0].flow_vol) == pytest.approx(0.001, rel=1e-3) assert value( sum(stream[0].conc_mass_phase_comp['Liq', j] for j in m.fs.properties.solute_set)) == pytest.approx(35.334, rel=1e-3) assert value( sum(stream[0].mass_frac_phase_comp['Liq', j] for j in m.fs.properties.solute_set)) == pytest.approx(35334e-6, rel=1e-3) assert value( sum(stream[0].mass_frac_phase_comp['Liq', j] for j in m.fs.properties.component_list)) == pytest.approx( 1, rel=1e-3) assert value( sum(stream[0].mole_frac_phase_comp['Liq', j] for j in m.fs.properties.component_list)) == pytest.approx( 1, rel=1e-3) assert value(stream[0].conc_mol_phase_comp['Liq', 'Na_+']) == pytest.approx( 483.565, rel=1e-3) assert value(stream[0].conc_mol_phase_comp['Liq', 'Cl_-']) == pytest.approx( 580, rel=1e-3) assert value(stream[0].conc_mol_phase_comp['Liq', 'Ca_2+']) == pytest.approx( 9.55, rel=1e-3) assert value(stream[0].conc_mol_phase_comp['Liq', 'SO4_2-']) == pytest.approx( 22.25, rel=1e-3) assert value(stream[0].conc_mol_phase_comp['Liq', 'Mg_2+']) == pytest.approx( 58.08, rel=1e-3) assert value(stream[0].conc_mass_phase_comp['Liq', 'Na_+']) == pytest.approx( 11.122, rel=1e-3) assert value(stream[0].conc_mass_phase_comp['Liq', 'Cl_-']) == pytest.approx( 20.3, rel=1e-3) assert value(stream[0].conc_mass_phase_comp['Liq', 'Ca_2+']) == pytest.approx( 0.382, rel=1e-3) assert value(stream[0].conc_mass_phase_comp['Liq', 'SO4_2-']) == pytest.approx( 2.136, rel=1e-3) assert value(stream[0].conc_mass_phase_comp['Liq', 'Mg_2+']) == pytest.approx( 1.394, rel=1e-3) assert value(stream[0].mole_frac_phase_comp['Liq', 'Na_+']) == pytest.approx( 8.833e-3, rel=1e-3) assert value(stream[0].mole_frac_phase_comp['Liq', 'Cl_-']) == pytest.approx( 1.059e-2, rel=1e-3) assert value(stream[0].mole_frac_phase_comp['Liq', 'Ca_2+']) == pytest.approx( 1.744e-4, rel=1e-3) assert value(stream[0].mole_frac_phase_comp['Liq', 'SO4_2-']) == pytest.approx( 4.064e-4, rel=1e-3) assert value(stream[0].mole_frac_phase_comp['Liq', 'Mg_2+']) == pytest.approx( 1.061e-3, rel=1e-3) assert value(stream[0].mass_frac_phase_comp['Liq', 'Na_+']) == pytest.approx( 1.112e-2, rel=1e-3) assert value(stream[0].mass_frac_phase_comp['Liq', 'Cl_-']) == pytest.approx( 2.03e-2, rel=1e-3) assert value(stream[0].mass_frac_phase_comp['Liq', 'Ca_2+']) == pytest.approx( 3.82e-4, rel=1e-3) assert value(stream[0].mass_frac_phase_comp['Liq', 'SO4_2-']) == pytest.approx( 2.136e-3, rel=1e-3) assert value(stream[0].mass_frac_phase_comp['Liq', 'Mg_2+']) == pytest.approx( 1.394e-3, rel=1e-3)
from VLE data to compute the activity coefficients. Author: Jaffer Ghouse """ import pytest from pyomo.environ import ConcreteModel from idaes.core import FlowsheetBlock from idaes.property_models.activity_coeff_models.BTX_activity_coeff_VLE \ import BTXParameterBlock from idaes.ui.report import degrees_of_freedom # ----------------------------------------------------------------------------- # Create a flowsheet for test m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) # vapor-liquid (NRTL) m.fs.properties_NRTL_vl = BTXParameterBlock(default={ "valid_phase": ('Liq', 'Vap'), "activity_coeff_model": 'NRTL' }) m.fs.state_block_NRTL_vl = m.fs.properties_NRTL_vl.state_block_class( default={ "parameters": m.fs.properties_NRTL_vl, "defined_state": True }) # liquid only (NRTL) m.fs.properties_NRTL_l = BTXParameterBlock(default={ "valid_phase": 'Liq',
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 test_T350_P1_x5(self): m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.props = BT_PR.BTParameterBlock( default={'valid_phase': ('Vap', 'Liq')}) m.fs.state = m.fs.props.build_state_block( default={'defined_state': True}) m.fs.state.flow_mol.fix(100) m.fs.state.mole_frac_comp["benzene"].fix(0.5) m.fs.state.mole_frac_comp["toluene"].fix(0.5) m.fs.state.temperature.fix(350) m.fs.state.pressure.fix(1e5) # Trigger build of enthalpy and entropy m.fs.state.enth_mol_phase m.fs.state.entr_mol_phase m.fs.state.initialize() solver.solve(m) assert pytest.approx(value(m.fs.state._teq), abs=1e-1) == 365 assert 0.0035346 == pytest.approx( value(m.fs.state.compress_fact_phase["Liq"]), 1e-5) assert 0.966749 == pytest.approx( value(m.fs.state.compress_fact_phase["Vap"]), 1e-5) assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "benzene"]), 1e-5) == 0.894676 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "toluene"]), 1e-5) == 0.347566 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "benzene"]), 1e-5) == 0.971072 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "toluene"]), 1e-5) == 0.959791 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "benzene"]), 1e-5) == 0.5 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "toluene"]), 1e-5) == 0.5 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "benzene"]), 1e-5) == 0.70584 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "toluene"]), 1e-5) == 0.29416 assert pytest.approx( value(m.fs.state.enth_mol_phase["Liq"]), 1e-5) == 38942.8 assert pytest.approx( value(m.fs.state.enth_mol_phase["Vap"]), 1e-5) == 78048.7 assert pytest.approx( value(m.fs.state.entr_mol_phase["Liq"]), 1e-5) == -367.558 assert pytest.approx( value(m.fs.state.entr_mol_phase["Vap"]), 1e-5) == -269.0553
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 test_T350_P5_x5(self): m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.props = BT_PR.BTParameterBlock( default={'valid_phase': ('Vap', 'Liq')}) m.fs.state = m.fs.props.build_state_block( default={'defined_state': True}) m.fs.state.flow_mol.fix(100) m.fs.state.mole_frac_comp["benzene"].fix(0.5) m.fs.state.mole_frac_comp["toluene"].fix(0.5) m.fs.state.temperature.fix(350) m.fs.state.pressure.fix(5e5) # Trigger build of enthalpy and entropy m.fs.state.enth_mol_phase m.fs.state.entr_mol_phase m.fs.state.initialize() solver.solve(m) assert pytest.approx(value(m.fs.state._teq), 1e-5) == 431.47 assert pytest.approx( value(m.fs.state.compress_fact_phase["Liq"]), 1e-5) == 0.01766 assert pytest.approx( value(m.fs.state.compress_fact_phase["Vap"]), 1e-5) == 0.80245 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "benzene"]), 1e-5) == 0.181229 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "toluene"]), 1e-5) == 0.070601 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "benzene"]), 1e-5) == 0.856523 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "toluene"]), 1e-5) == 0.799237 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "benzene"]), 1e-5) == 0.5 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "toluene"]), 1e-5) == 0.5 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "benzene"]), 1e-5) == 0.65415 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "toluene"]), 1e-5) == 0.34585 assert pytest.approx( value(m.fs.state.enth_mol_phase["Liq"]), 1e-5) == 38966.9 assert pytest.approx( value(m.fs.state.enth_mol_phase["Vap"]), 1e-5) == 75150.7 assert pytest.approx( value(m.fs.state.entr_mol_phase["Liq"]), 1e-5) == -367.6064 assert pytest.approx( value(m.fs.state.entr_mol_phase["Vap"]), 1e-5) == -287.3318
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 test_T450_P1_x5(self): m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.props = BT_PR.BTParameterBlock( default={'valid_phase': ('Vap', 'Liq')}) m.fs.state = m.fs.props.build_state_block( default={'defined_state': True}) m.fs.state.flow_mol.fix(100) m.fs.state.mole_frac_comp["benzene"].fix(0.5) m.fs.state.mole_frac_comp["toluene"].fix(0.5) m.fs.state.temperature.fix(450) m.fs.state.pressure.fix(1e5) # Trigger build of enthalpy and entropy m.fs.state.enth_mol_phase m.fs.state.entr_mol_phase m.fs.state.initialize() solver.solve(m) assert pytest.approx(value(m.fs.state._teq), 1e-5) == 371.4 assert 0.0033583 == pytest.approx( value(m.fs.state.compress_fact_phase["Liq"]), 1e-5) assert 0.9821368 == pytest.approx( value(m.fs.state.compress_fact_phase["Vap"]), 1e-5) assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "benzene"]), 1e-5) == 8.069323 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "toluene"]), 1e-5) == 4.304955 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "benzene"]), 1e-5) == 0.985365 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "toluene"]), 1e-5) == 0.979457 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "benzene"]), 1e-5) == 0.29861 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "toluene"]), 1e-5) == 0.70139 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "benzene"]), 1e-5) == 0.5 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "toluene"]), 1e-5) == 0.5 assert pytest.approx( value(m.fs.state.enth_mol_phase["Liq"]), 1e-5) == 49441.2 assert pytest.approx( value(m.fs.state.enth_mol_phase["Vap"]), 1e-5) == 84175.1 assert pytest.approx( value(m.fs.state.entr_mol_phase["Liq"]), 1e-5) == -333.836 assert pytest.approx( value(m.fs.state.entr_mol_phase["Vap"]), 1e-5) == -247.385
def build_unit(): # Create a Concrete Model as the top level object m = pyo.ConcreteModel() # Add a flowsheet object to the model m.fs = FlowsheetBlock(default={"dynamic": False}) # Add property packages to flowsheet library m.fs.prop_water = iapws95.Iapws95ParameterBlock() m.fs.prop_fluegas = FlueGasParameterBlock() # Add a 2D cross-flow heat exchanger with header model to the flowsheet m.fs.unit = HeatExchangerCrossFlow2D_Header( default={ "tube_side": { "property_package": m.fs.prop_water, "has_pressure_change": True }, "shell_side": { "property_package": m.fs.prop_fluegas, "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, "tube_inner_diameter": 0.035, "tube_thickness": 0.0035, "has_header": True, "header_radial_elements": 5, "header_inner_diameter": 0.3, "header_wall_thickness": 0.03 }) # Primary Superheater (NETL baseline report) ITM = 0.0254 # inch to meter conversion # m.fs.unit.tube_di.fix((2.5-2*0.165)*ITM) # m.fs.unit.tube_thickness.fix(0.165*ITM) m.fs.unit.pitch_x.fix(3 * ITM) # gas path transverse width 54.78 ft / number of columns m.fs.unit.pitch_y.fix(54.78 / 108 * 12 * ITM) m.fs.unit.tube_length_seg.fix(53.13 * 12 * ITM) m.fs.unit.tube_nseg.fix(20 * 2) m.fs.unit.tube_ncol.fix(108) m.fs.unit.tube_inlet_nrow.fix(4) m.fs.unit.delta_elevation.fix(50) m.fs.unit.tube_r_fouling = 0.000176 # (0.001 h-ft^2-F/BTU) m.fs.unit.tube_r_fouling = 0.003131 # (0.03131 - 0.1779 h-ft^2-F/BTU) # material inputs m.fs.unit.therm_cond_wall = 43.0 # Carbon steel 1% C in W/m/K m.fs.unit.dens_wall = 7850 # kg/m3 or 0.284 lb/in3 m.fs.unit.cp_wall = 510.8 # J/kg-K (0.05 to 0.25 % C) m.fs.unit.Young_modulus = 2.00e5 # 200 GPa (29,000 ksi) m.fs.unit.Possion_ratio = 0.29 m.fs.unit.coefficient_therm_expansion = 1.3e-5 m.fs.unit.emissivity_wall.fix(0.7) m.fs.unit.fcorrection_htc_tube.fix(1.0) m.fs.unit.fcorrection_htc_shell.fix(1.0) m.fs.unit.fcorrection_dp_tube.fix(1.0) m.fs.unit.fcorrection_dp_shell.fix(1.0) m.fs.unit.temperature_ambient.fix(350.0) m.fs.unit.head_insulation_thickness.fix(0.025) return m
def test_T450_P5_x5(self): m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.props = BT_PR.BTParameterBlock( default={'valid_phase': ('Vap', 'Liq')}) m.fs.state = m.fs.props.build_state_block( default={'defined_state': True}) m.fs.state.flow_mol.fix(100) m.fs.state.mole_frac_comp["benzene"].fix(0.5) m.fs.state.mole_frac_comp["toluene"].fix(0.5) m.fs.state.temperature.fix(450) m.fs.state.pressure.fix(5e5) # Trigger build of enthalpy and entropy m.fs.state.enth_mol_phase m.fs.state.entr_mol_phase m.fs.state.initialize() solver.solve(m) assert pytest.approx(value(m.fs.state._teq), 1e-5) == 436.93 assert 0.0166181 == pytest.approx( value(m.fs.state.compress_fact_phase["Liq"]), 1e-5) assert 0.9053766 == pytest.approx( value(m.fs.state.compress_fact_phase["Vap"]), 1e-5) assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "benzene"]), 1e-5) == 1.63308 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Liq", "toluene"]), 1e-5) == 0.873213 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "benzene"]), 1e-5) == 0.927534 assert pytest.approx( value(m.fs.state.fug_coeff_phase_comp["Vap", "toluene"]), 1e-5) == 0.898324 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "benzene"]), 1e-5) == 0.3488737 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Liq", "toluene"]), 1e-5) == 0.6511263 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "benzene"]), 1e-5) == 0.5 assert pytest.approx( value(m.fs.state.mole_frac_phase_comp["Vap", "toluene"]), 1e-5) == 0.5 assert pytest.approx( value(m.fs.state.enth_mol_phase["Liq"]), 1e-5) == 51095.2 assert pytest.approx( value(m.fs.state.enth_mol_phase["Vap"]), 1e-5) == 83362.3 assert pytest.approx( value(m.fs.state.entr_mol_phase["Liq"]), 1e-5) == -331.676 assert pytest.approx( value(m.fs.state.entr_mol_phase["Vap"]), 1e-5) == -261.961
def build_turbine(): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = iapws95.Iapws95ParameterBlock() m.fs.turb = HelmTurbineOutletStage(default={"property_package": m.fs.properties}) return m
def test_Pdrop_fixed_per_unit_length(self): """ Testing 0D-RO with PressureChangeType.fixed_per_unit_length option. """ m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = props.NaClParameterBlock() m.fs.unit = ReverseOsmosis0D( default={ "property_package": m.fs.properties, "has_pressure_change": True, "concentration_polarization_type": ConcentrationPolarizationType.calculated, "mass_transfer_coefficient": MassTransferCoefficient.calculated, "pressure_change_type": PressureChangeType.fixed_per_unit_length }) # fully specify system feed_flow_mass = 1 feed_mass_frac_NaCl = 0.035 feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl feed_pressure = 50e5 feed_temperature = 273.15 + 25 membrane_area = 50 length = 20 A = 4.2e-12 B = 3.5e-8 pressure_atmospheric = 101325 membrane_pressure_drop = 3e5 m.fs.unit.inlet.flow_mass_phase_comp[0, 'Liq', 'NaCl'].fix( feed_flow_mass * feed_mass_frac_NaCl) m.fs.unit.inlet.flow_mass_phase_comp[0, 'Liq', 'H2O'].fix( feed_flow_mass * feed_mass_frac_H2O) m.fs.unit.inlet.pressure[0].fix(feed_pressure) m.fs.unit.inlet.temperature[0].fix(feed_temperature) m.fs.unit.area.fix(membrane_area) m.fs.unit.A_comp.fix(A) m.fs.unit.B_comp.fix(B) m.fs.unit.permeate.pressure[0].fix(pressure_atmospheric) m.fs.unit.channel_height.fix(0.002) m.fs.unit.spacer_porosity.fix(0.75) m.fs.unit.length.fix(length) m.fs.unit.dP_dx.fix(-membrane_pressure_drop / length) # test statistics assert number_variables(m) == 110 assert number_total_constraints(m) == 80 assert number_unused_variables(m) == 0 # test degrees of freedom assert degrees_of_freedom(m) == 0 # test 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')) calculate_scaling_factors(m) # check that all variables have scaling factors. # TODO: see aforementioned TODO on revisiting scaling and associated testing for property models. unscaled_var_list = list( unscaled_variables_generator(m.fs.unit, include_fixed=True)) assert len(unscaled_var_list) == 0 # check that all constraints have been scaled unscaled_constraint_list = list(unscaled_constraints_generator(m)) assert len(unscaled_constraint_list) == 0 # test initialization initialization_tester(m) # test variable scaling badly_scaled_var_lst = list(badly_scaled_var_generator(m)) assert badly_scaled_var_lst == [] # test solve solver.options = {'nlp_scaling_method': 'user-scaling'} results = solver.solve(m, tee=True) # Check for optimal solution assert results.solver.termination_condition == \ TerminationCondition.optimal assert results.solver.status == SolverStatus.ok # test solution assert (pytest.approx(-3.000e5, rel=1e-3) == value(m.fs.unit.deltaP[0])) assert (pytest.approx(2.205e-3, rel=1e-3) == value( m.fs.unit.flux_mass_phase_comp_avg[0, 'Liq', 'H2O'])) assert (pytest.approx(1.826e-6, rel=1e-3) == value( m.fs.unit.flux_mass_phase_comp_avg[0, 'Liq', 'NaCl'])) assert (pytest.approx(0.1103, rel=1e-3) == value( m.fs.unit.properties_permeate[0].flow_mass_phase_comp['Liq', 'H2O'])) assert (pytest.approx(9.129e-5, rel=1e-3) == value( m.fs.unit.properties_permeate[0].flow_mass_phase_comp['Liq', 'NaCl'])) assert (pytest.approx(35.751, rel=1e-3) == value( m.fs.unit.feed_side.properties_in[0].conc_mass_phase_comp['Liq', 'NaCl'])) assert (pytest.approx(53.506, rel=1e-3) == value( m.fs.unit.feed_side.properties_interface_in[0]. conc_mass_phase_comp['Liq', 'NaCl'])) assert (pytest.approx(40.207, rel=1e-3) == value( m.fs.unit.feed_side.properties_out[0].conc_mass_phase_comp['Liq', 'NaCl']) ) assert (pytest.approx(52.476, rel=1e-3) == value( m.fs.unit.feed_side.properties_interface_out[0]. conc_mass_phase_comp['Liq', 'NaCl']))