def test_config(): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = props.NaClParameterBlock() m.fs.unit = ReverseOsmosis0D(default={"property_package": m.fs.properties}) assert len(m.fs.unit.config) == 11 assert not m.fs.unit.config.dynamic assert not m.fs.unit.config.has_holdup assert m.fs.unit.config.material_balance_type == \ MaterialBalanceType.useDefault assert m.fs.unit.config.energy_balance_type == \ EnergyBalanceType.useDefault assert m.fs.unit.config.momentum_balance_type == \ MomentumBalanceType.pressureTotal assert not m.fs.unit.config.has_pressure_change assert m.fs.unit.config.property_package is \ m.fs.properties assert m.fs.unit.config.concentration_polarization_type == \ ConcentrationPolarizationType.none assert m.fs.unit.config.mass_transfer_coefficient == \ MassTransferCoefficient.none assert m.fs.unit.config.pressure_change_type == \ PressureChangeType.fixed_per_stage
def test_option_pressure_change_calculated(): 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.none, "mass_transfer_coefficient": MassTransferCoefficient.none, "pressure_change_type": PressureChangeType.calculated }) assert m.fs.unit.config.concentration_polarization_type == \ ConcentrationPolarizationType.none assert m.fs.unit.config.mass_transfer_coefficient == \ MassTransferCoefficient.none assert m.fs.unit.config.pressure_change_type == \ PressureChangeType.calculated assert isinstance(m.fs.unit.feed_side.deltaP, Var) assert isinstance(m.fs.unit.deltaP, Var) assert isinstance(m.fs.unit.channel_height, Var) assert isinstance(m.fs.unit.width, Var) assert isinstance(m.fs.unit.length, Var) assert isinstance(m.fs.unit.dh, Var) assert isinstance(m.fs.unit.spacer_porosity, Var) assert isinstance(m.fs.unit.N_Re_io, Var)
def test_option_has_pressure_change(): 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 }) assert isinstance(m.fs.unit.feed_side.deltaP, Var) assert isinstance(m.fs.unit.deltaP, Var)
def test_option_concentration_polarization_type_fixed(): 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.fixed }) assert m.fs.unit.config.concentration_polarization_type == \ ConcentrationPolarizationType.fixed assert isinstance(m.fs.unit.cp_modulus, Var)
def frame(self): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = props.NaClParameterBlock() m.fs.stream = m.fs.properties.build_state_block([0], default={}) # specify conditions mass_flow = 1 x_NaCl = 0.035 m.fs.stream[0].flow_mass_phase_comp['Liq', 'NaCl'].fix(x_NaCl * mass_flow) m.fs.stream[0].flow_mass_phase_comp['Liq', 'H2O'].fix( (1 - x_NaCl) * mass_flow) m.fs.stream[0].temperature.fix(273.15 + 25) m.fs.stream[0].pressure.fix(101325) return m
def test_option_concentration_polarization_type_calculated_kf_fixed(): 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 }) assert m.fs.unit.config.concentration_polarization_type == \ ConcentrationPolarizationType.calculated assert m.fs.unit.config.mass_transfer_coefficient == \ MassTransferCoefficient.fixed assert isinstance(m.fs.unit.Kf_io, Var)
def RO_frame(self): 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.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 concentration_polarization_modulus = 1.1 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.cp_modulus.fix(concentration_polarization_modulus) return m
def NF_frame(self): m = ConcreteModel() m.fs = FlowsheetBlock(default={"dynamic": False}) m.fs.properties = props.NaClParameterBlock() m.fs.unit = NanoFiltration0D(default={ "property_package": m.fs.properties, "has_pressure_change": True, }) # fully specify system feed_flow_mass = 1 feed_mass_frac_NaCl = 0.035 feed_pressure = 6e5 feed_temperature = 273.15 + 25 membrane_pressure_drop = 1e5 membrane_area = 50 * feed_flow_mass A = 3.77e-11 B = 4.724e-5 sigma = 0.28 pressure_atmospheric = 101325 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.sigma.fix(sigma) m.fs.unit.permeate.pressure[0].fix(pressure_atmospheric) return m
def build(): # flowsheet set up m = ConcreteModel() m.fs = FlowsheetBlock(default={'dynamic': False}) m.fs.properties = props.NaClParameterBlock() financials.add_costing_param_block(m.fs) # unit models m.fs.feed = Feed(default={'property_package': m.fs.properties}) m.fs.S1 = Separator(default={ "property_package": m.fs.properties, "outlet_list": ['P1', 'PXR'] }) m.fs.P1 = Pump(default={'property_package': m.fs.properties}) m.fs.PXR = PressureExchanger(default={'property_package': m.fs.properties}) m.fs.P2 = Pump(default={'property_package': m.fs.properties}) m.fs.M1 = Mixer( default={ "property_package": m.fs.properties, "momentum_mixing_type": MomentumMixingType.equality, # booster pump will match pressure "inlet_list": ['P1', 'P2'] }) m.fs.RO = ReverseOsmosis0D(default={ "property_package": m.fs.properties, "has_pressure_change": True }) m.fs.product = Product(default={'property_package': m.fs.properties}) m.fs.disposal = Product(default={'property_package': m.fs.properties}) # additional variables or expressions feed_flow_vol_total = m.fs.feed.properties[0].flow_vol product_flow_vol_total = m.fs.product.properties[0].flow_vol m.fs.recovery = Expression(expr=product_flow_vol_total / feed_flow_vol_total) m.fs.annual_water_production = Expression(expr=pyunits.convert( product_flow_vol_total, to_units=pyunits.m**3 / pyunits.year) * m.fs.costing_param.load_factor) pump_power_total = m.fs.P1.work_mechanical[0] + m.fs.P2.work_mechanical[0] m.fs.specific_energy_consumption = Expression( expr=pyunits.convert(pump_power_total, to_units=pyunits.kW) / pyunits.convert(product_flow_vol_total, to_units=pyunits.m**3 / pyunits.hr)) # costing m.fs.P1.get_costing(module=financials, pump_type="High pressure") m.fs.P2.get_costing(module=financials, pump_type="High pressure") m.fs.RO.get_costing(module=financials) m.fs.PXR.get_costing(module=financials) financials.get_system_costing(m.fs) # connections m.fs.s01 = Arc(source=m.fs.feed.outlet, destination=m.fs.S1.inlet) m.fs.s02 = Arc(source=m.fs.S1.P1, destination=m.fs.P1.inlet) m.fs.s03 = Arc(source=m.fs.P1.outlet, destination=m.fs.M1.P1) m.fs.s04 = Arc(source=m.fs.M1.outlet, destination=m.fs.RO.inlet) m.fs.s05 = Arc(source=m.fs.RO.permeate, destination=m.fs.product.inlet) m.fs.s06 = Arc(source=m.fs.RO.retentate, destination=m.fs.PXR.high_pressure_inlet) m.fs.s07 = Arc(source=m.fs.PXR.high_pressure_outlet, destination=m.fs.disposal.inlet) m.fs.s08 = Arc(source=m.fs.S1.PXR, destination=m.fs.PXR.low_pressure_inlet) m.fs.s09 = Arc(source=m.fs.PXR.low_pressure_outlet, destination=m.fs.P2.inlet) m.fs.s10 = Arc(source=m.fs.P2.outlet, destination=m.fs.M1.P2) TransformationFactory("network.expand_arcs").apply_to(m) # scaling m.fs.properties.set_default_scaling('flow_mass_phase_comp', 1, index=('Liq', 'H2O')) m.fs.properties.set_default_scaling('flow_mass_phase_comp', 1e2, index=('Liq', 'NaCl')) iscale.calculate_scaling_factors(m) return m
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']))
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_io - 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_io[0, 'in', 'NaCl'].fix(kf) m.fs.unit.Kf_io[0, 'out', 'NaCl'].fix(kf) # test statistics assert number_variables(m) == 94 assert number_total_constraints(m) == 65 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 # 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) # Check for optimal solution assert results.solver.termination_condition == \ TerminationCondition.optimal assert results.solver.status == SolverStatus.ok # test solution assert (pytest.approx(3.807e-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.1904, rel=1e-3) == value( m.fs.unit.properties_permeate[0].flow_mass_phase_comp['Liq', 'H2O'])) assert (pytest.approx(8.342e-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(46.123, rel=1e-3) == value( m.fs.unit.feed_side.properties_interface_in[0]. conc_mass_phase_comp['Liq', 'NaCl'])) assert (pytest.approx(44.321, rel=1e-3) == value( m.fs.unit.feed_side.properties_out[0].conc_mass_phase_comp['Liq', 'NaCl']) ) assert (pytest.approx(50.081, rel=1e-3) == value( m.fs.unit.feed_side.properties_interface_out[0]. conc_mass_phase_comp['Liq', 'NaCl']))