diff --git a/watertap/unit_models/osmotically_assisted_reverse_osmosis_base.py b/watertap/unit_models/osmotically_assisted_reverse_osmosis_base.py index 3725dc177c..1052c83178 100644 --- a/watertap/unit_models/osmotically_assisted_reverse_osmosis_base.py +++ b/watertap/unit_models/osmotically_assisted_reverse_osmosis_base.py @@ -35,6 +35,7 @@ from watertap.core.membrane_channel_base import ( validate_membrane_config_args, ConcentrationPolarizationType, + TransportModel, ) from watertap.core import InitializationMixin @@ -386,6 +387,21 @@ def _add_flux_balance(self): doc="Pure water density", ) + if self.config.transport_model == TransportModel.SKK: + self.reflect_coeff = Var( + initialize=0.9, + domain=NonNegativeReals, + units=pyunits.dimensionless, + doc="Reflection coefficient of the membrane", + ) + + self.alpha = Var( + initialize=1e8, + domain=NonNegativeReals, + units=units_meta("time") * units_meta("length") ** -1, + doc="Alpha coefficient of the membrane", + ) + self.flux_mass_phase_comp = Var( self.flowsheet().config.time, self.difference_elements, @@ -399,34 +415,84 @@ def _add_flux_balance(self): doc="Mass flux across membrane at inlet and outlet", ) - @self.Constraint( - self.flowsheet().config.time, - self.difference_elements, - self.config.property_package.phase_list, - self.config.property_package.component_list, - doc="Solvent and solute mass flux", - ) - def eq_flux_mass(b, t, x, p, j): - prop_feed = b.feed_side.properties[t, x] - prop_perm = b.permeate_side.properties[t, x] - interface_feed = b.feed_side.properties_interface[t, x] - interface_perm = b.permeate_side.properties_interface[t, x] - comp = self.config.property_package.get_component(j) - if comp.is_solvent(): - return b.flux_mass_phase_comp[t, x, p, j] == b.A_comp[ - t, j - ] * b.dens_solvent * ( - (prop_feed.pressure - prop_perm.pressure) - - ( - interface_feed.pressure_osm_phase[p] - - interface_perm.pressure_osm_phase[p] + if self.config.transport_model == TransportModel.SD: + + @self.Constraint( + self.flowsheet().config.time, + self.difference_elements, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Solvent and solute mass flux", + ) + def eq_flux_mass(b, t, x, p, j): + prop_feed = b.feed_side.properties[t, x] + prop_perm = b.permeate_side.properties[t, x] + interface_feed = b.feed_side.properties_interface[t, x] + interface_perm = b.permeate_side.properties_interface[t, x] + comp = self.config.property_package.get_component(j) + if comp.is_solvent(): + return b.flux_mass_phase_comp[t, x, p, j] == b.A_comp[ + t, j + ] * b.dens_solvent * ( + (prop_feed.pressure - prop_perm.pressure) + - ( + interface_feed.pressure_osm_phase[p] + - interface_perm.pressure_osm_phase[p] + ) + ) + elif comp.is_solute(): + return b.flux_mass_phase_comp[t, x, p, j] == b.B_comp[t, j] * ( + interface_feed.conc_mass_phase_comp[p, j] + - interface_perm.conc_mass_phase_comp[p, j] + ) + + elif self.config.transport_model == TransportModel.SKK: + + @self.Constraint( + self.flowsheet().config.time, solute_set, doc="SKK alpha coeff." + ) + def eq_alpha(b, t, j): + return b.alpha == (1 - b.reflect_coeff) / b.B_comp[t, j] + + @self.Constraint( + self.flowsheet().config.time, + self.difference_elements, + self.config.property_package.phase_list, + self.config.property_package.component_list, + doc="Solvent and solute mass flux using SKK model", + ) + def eq_flux_mass(b, t, x, p, j): + prop_feed = b.feed_side.properties[t, x] + prop_perm = b.permeate_side.properties[t, x] + interface_feed = b.feed_side.properties_interface[t, x] + interface_perm = b.permeate_side.properties_interface[t, x] + comp = self.config.property_package.get_component(j) + if comp.is_solvent(): + return b.flux_mass_phase_comp[t, x, p, j] == b.A_comp[ + t, j + ] * b.dens_solvent * ( + (prop_feed.pressure - prop_perm.pressure) + - b.reflect_coeff + * ( + interface_feed.pressure_osm_phase[p] + - interface_perm.pressure_osm_phase[p] + ) + ) + elif comp.is_solute(): + return b.flux_mass_phase_comp[t, x, p, j] == b.B_comp[t, j] * ( + interface_feed.conc_mass_phase_comp[p, j] + - interface_perm.conc_mass_phase_comp[p, j] + ) + (1 - b.reflect_coeff) * ( + ( + (b.flux_mass_phase_comp[t, x, p, "H2O"] / b.dens_solvent) + * interface_feed.conc_mass_phase_comp[p, j] + ) ) - ) - elif comp.is_solute(): - return b.flux_mass_phase_comp[t, x, p, j] == b.B_comp[t, j] * ( - interface_feed.conc_mass_phase_comp[p, j] - - interface_perm.conc_mass_phase_comp[p, j] - ) + + else: + raise ConfigurationError( + "Unsupported transport model: {}".format(self.config.transport_model) + ) @self.Expression( self.flowsheet().config.time, @@ -811,6 +877,13 @@ def calculate_scaling_factors(self): if iscale.get_scaling_factor(self.recovery_vol_phase) is None: iscale.set_scaling_factor(self.recovery_vol_phase, 1) + if self.config.transport_model == TransportModel.SKK: + if iscale.get_scaling_factor(self.alpha) is None: + iscale.set_scaling_factor(self.alpha, 1e-8) + + if iscale.get_scaling_factor(self.reflect_coeff) is None: + iscale.set_scaling_factor(self.reflect_coeff, 1) + for (t, p, j), v in self.recovery_mass_phase_comp.items(): if j in self.config.property_package.solvent_set: sf = 1 diff --git a/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_0D.py b/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_0D.py index f0ea13c1a4..c3e16ceae2 100644 --- a/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_0D.py +++ b/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_0D.py @@ -31,6 +31,7 @@ from watertap.unit_models.osmotically_assisted_reverse_osmosis_0D import ( OsmoticallyAssistedReverseOsmosis0D, ) +from watertap.unit_models.reverse_osmosis_base import TransportModel import watertap.property_models.NaCl_prop_pack as props from idaes.core.solvers import get_solver @@ -235,6 +236,24 @@ def test_option_friction_factor_spiral_wound(): assert isinstance(m.fs.unit.permeate_side.eq_friction_factor, Constraint) +@pytest.mark.unit +def test_option_has_mass_transfer_model(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = props.NaClParameterBlock() + m.fs.unit = OsmoticallyAssistedReverseOsmosis0D( + property_package=m.fs.properties, transport_model=TransportModel.SKK + ) + + assert isinstance(m.fs.unit.reflect_coeff, Var) + assert isinstance(m.fs.unit.alpha, Var) + + assert value(m.fs.unit.reflect_coeff) == 0.9 + + assert pytest.approx(0.9, rel=1e-3) == value(m.fs.unit.reflect_coeff) + assert pytest.approx(1e8, rel=1e-3) == value(m.fs.unit.alpha) + + class TestOsmoticallyAssistedReverseOsmosis: @pytest.fixture(scope="class") def RO_frame(self): @@ -465,6 +484,237 @@ def test_solution(self, RO_frame): assert pytest.approx(0, abs=1e-3) == value(m.fs.unit.feed_side.deltaP[0]) assert pytest.approx(0, abs=1e-3) == value(m.fs.unit.permeate_side.deltaP[0]) + @pytest.fixture(scope="class") + def RO_SKK_frame(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = OsmoticallyAssistedReverseOsmosis0D( + property_package=m.fs.properties, + has_pressure_change=True, + concentration_polarization_type=ConcentrationPolarizationType.fixed, + mass_transfer_coefficient=MassTransferCoefficient.none, + transport_model=TransportModel.SKK, + has_full_reporting=True, + ) + + # fully specify system + feed_flow_mass = 1 + feed_mass_frac_NaCl = 0.035 + feed_pressure = 50e5 + feed_temperature = 273.15 + 25 + membrane_pressure_drop = -0.5e5 + membrane_area = 50 + A = 4.2e-12 + B = 1.3e-8 + pressure_atmospheric = 101325 + feed_cp_mod = 1.1 + permeate_cp_mod = 0.9 + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + m.fs.unit.feed_inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.feed_inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + + m.fs.unit.feed_inlet.pressure[0].fix(feed_pressure) + m.fs.unit.feed_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.reflect_coeff.fix(0.9) + m.fs.unit.feed_side.cp_modulus.fix(feed_cp_mod) + + m.fs.unit.permeate_inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + 0.16189443280346605 + ) + m.fs.unit.permeate_inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + 0.0024736353420558967 + ) + m.fs.unit.permeate_inlet.pressure[0].fix(101325) + m.fs.unit.permeate_inlet.temperature[0].fix(feed_temperature) + + m.fs.unit.permeate_side.cp_modulus.fix(permeate_cp_mod) + m.fs.unit.feed_side.deltaP.fix(0) + m.fs.unit.permeate_side.deltaP.fix(0) + + return m + + @pytest.mark.unit + def test_skk_build(self, RO_SKK_frame): + m = RO_SKK_frame + + # test ports + port_lst = ["feed_inlet", "feed_outlet", "permeate_inlet", "permeate_outlet"] + for port_str in port_lst: + port = getattr(m.fs.unit, port_str) + assert isinstance(port, Port) + # number of state variables for NaCl property package + assert len(port.vars) == 3 + + # test feed-side control volume and associated stateblocks + assert isinstance(m.fs.unit.feed_side, MembraneChannel0DBlock) + assert isinstance(m.fs.unit.permeate_side, MembraneChannel0DBlock) + + # test statistics + assert number_variables(m) == 130 + assert number_total_constraints(m) == 99 + assert number_unused_variables(m) == 8 + + @pytest.mark.unit + def test_skk_dof(self, RO_SKK_frame): + m = RO_SKK_frame + assert degrees_of_freedom(m) == 0 + + @pytest.mark.unit + def test_skk_calculate_scaling(self, RO_SKK_frame): + m = RO_SKK_frame + + 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 + unscaled_var_list = list(unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + + for i in badly_scaled_var_generator(m): + print(i[0].name, i[1]) + + @pytest.mark.component + def test_skk_initialize(self, RO_SKK_frame): + initialization_tester(RO_SKK_frame, outlvl=idaeslog.DEBUG) + + @pytest.mark.component + def test_skk_var_scaling(self, RO_SKK_frame): + m = RO_SKK_frame + badly_scaled_var_lst = list(badly_scaled_var_generator(m)) + [print(i[0], i[1]) for i in badly_scaled_var_lst] + assert badly_scaled_var_lst == [] + + @pytest.mark.component + def test_skk_solve(self, RO_SKK_frame): + m = RO_SKK_frame + results = solver.solve(m) + + # Check for optimal solution + assert_optimal_termination(results) + + @pytest.mark.component + def test_skk_conservation(self, RO_SKK_frame): + m = RO_SKK_frame + b = m.fs.unit + comp_lst = ["NaCl", "H2O"] + + feed_flow_mass_inlet = sum( + b.feed_side.properties_in[0].flow_mass_phase_comp["Liq", j] + for j in comp_lst + ) + feed_flow_mass_outlet = sum( + b.feed_side.properties_out[0].flow_mass_phase_comp["Liq", j] + for j in comp_lst + ) + perm_flow_mass_inlet = sum( + b.permeate_side.properties_in[0].flow_mass_phase_comp["Liq", j] + for j in comp_lst + ) + perm_flow_mass_outlet = sum( + b.permeate_side.properties_out[0].flow_mass_phase_comp["Liq", j] + for j in comp_lst + ) + + assert ( + abs( + value( + feed_flow_mass_inlet + + perm_flow_mass_inlet + - feed_flow_mass_outlet + - perm_flow_mass_outlet + ) + ) + <= 1e-5 + ) + + assert ( + abs( + value( + feed_flow_mass_inlet + * b.feed_side.properties_in[0].enth_mass_phase["Liq"] + - feed_flow_mass_outlet + * b.feed_side.properties_out[0].enth_mass_phase["Liq"] + + perm_flow_mass_inlet + * b.permeate_side.properties_in[0].enth_mass_phase["Liq"] + - perm_flow_mass_outlet + * b.permeate_side.properties_out[0].enth_mass_phase["Liq"] + ) + ) + <= 1e-5 + ) + + @pytest.mark.component + def test_skk_solution(self, RO_SKK_frame): + m = RO_SKK_frame + assert pytest.approx(7.971e-03, rel=1e-3) == value( + m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "H2O"] + ) + assert pytest.approx(3.801e-5, rel=1e-3) == value( + m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "NaCl"] + ) + assert pytest.approx(0.56644, rel=1e-3) == value( + m.fs.unit.feed_outlet.flow_mass_phase_comp[0, "Liq", "H2O"] + ) + assert pytest.approx(0.03309, rel=1e-3) == value( + m.fs.unit.feed_outlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ) + assert pytest.approx( + value(m.fs.unit.feed_side.cp_modulus[0, 0.0, "NaCl"]), rel=1e-3 + ) == value( + m.fs.unit.feed_side.properties_interface[0, 0.0].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) / value( + m.fs.unit.feed_side.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx( + value(m.fs.unit.feed_side.cp_modulus[0, 1.0, "NaCl"]), rel=1e-3 + ) == value( + m.fs.unit.feed_side.properties_interface[0, 1.0].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) / value( + m.fs.unit.feed_side.properties_out[0].conc_mass_phase_comp["Liq", "NaCl"] + ) + + assert pytest.approx( + value(m.fs.unit.permeate_side.cp_modulus[0, 0.0, "NaCl"]), rel=1e-3 + ) == value( + m.fs.unit.permeate_side.properties_interface[0, 0.0].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) / value( + m.fs.unit.permeate_side.properties_out[0].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) + assert pytest.approx( + value(m.fs.unit.permeate_side.cp_modulus[0, 1.0, "NaCl"]), rel=1e-3 + ) == value( + m.fs.unit.permeate_side.properties_interface[0, 1.0].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) / value( + m.fs.unit.permeate_side.properties_in[0].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx(0, abs=1e-3) == value(m.fs.unit.feed_side.deltaP[0]) + assert pytest.approx(0, abs=1e-3) == value(m.fs.unit.permeate_side.deltaP[0]) + @pytest.mark.component def test_CP_calculation_with_kf_fixed(self): """Testing 0D-OARO with ConcentrationPolarizationType.calculated option enabled. diff --git a/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_1D.py b/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_1D.py index 9fdb62f83c..d5d28281a7 100644 --- a/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_1D.py +++ b/watertap/unit_models/tests/test_osmotically_assisted_reverse_osmosis_1D.py @@ -31,6 +31,7 @@ from watertap.unit_models.osmotically_assisted_reverse_osmosis_1D import ( OsmoticallyAssistedReverseOsmosis1D, ) +from watertap.unit_models.reverse_osmosis_base import TransportModel import watertap.property_models.NaCl_prop_pack as props from idaes.core.solvers import get_solver @@ -222,6 +223,24 @@ def test_option_pressure_change_calculated(): assert isinstance(m.fs.unit.eq_area, Constraint) +@pytest.mark.unit +def test_option_has_mass_transfer_model(): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + m.fs.properties = props.NaClParameterBlock() + m.fs.unit = OsmoticallyAssistedReverseOsmosis1D( + property_package=m.fs.properties, transport_model=TransportModel.SKK + ) + + assert isinstance(m.fs.unit.reflect_coeff, Var) + assert isinstance(m.fs.unit.alpha, Var) + + assert value(m.fs.unit.reflect_coeff) == 0.9 + + assert pytest.approx(0.9, rel=1e-3) == value(m.fs.unit.reflect_coeff) + assert pytest.approx(1e8, rel=1e-3) == value(m.fs.unit.alpha) + + class TestOsmoticallyAssistedReverseOsmosis: @pytest.fixture(scope="class") def RO_frame(self): @@ -470,6 +489,486 @@ def test_solution(self, RO_frame): m.fs.unit.permeate_side.deltaP_stage[0] ) + @pytest.fixture(scope="class") + def RO_SKK_frame(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = OsmoticallyAssistedReverseOsmosis1D( + property_package=m.fs.properties, + has_pressure_change=True, + concentration_polarization_type=ConcentrationPolarizationType.fixed, + mass_transfer_coefficient=MassTransferCoefficient.none, + transport_model=TransportModel.SKK, + has_full_reporting=True, + ) + + # fully specify system + feed_flow_mass = 5 / 18 + feed_mass_frac_NaCl = 0.075 + feed_pressure = 65e5 + feed_temperature = 273.15 + 25 + membrane_area = 155 + width = 1.1 + A = 1e-12 + B = 7.7e-8 + + feed_cp_mod = 1.05 + permeate_cp_mod = 0.9 + + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + m.fs.unit.feed_inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.feed_inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.feed_inlet.pressure[0].fix(feed_pressure) + m.fs.unit.feed_inlet.temperature[0].fix(feed_temperature) + m.fs.unit.feed_side.cp_modulus.fix(feed_cp_mod) + m.fs.unit.feed_side.deltaP_stage.fix(0) + + permeate_flow_mass = 0.33 * feed_flow_mass + permeate_mass_frac_NaCl = 0.1 + permeate_mass_frac_H2O = 1 - permeate_mass_frac_NaCl + m.fs.unit.permeate_inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + permeate_flow_mass * permeate_mass_frac_H2O + ) + m.fs.unit.permeate_inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + permeate_flow_mass * permeate_mass_frac_NaCl + ) + m.fs.unit.permeate_inlet.pressure[0].fix(5e5) + m.fs.unit.permeate_inlet.temperature[0].fix(feed_temperature) + m.fs.unit.permeate_side.cp_modulus.fix(permeate_cp_mod) + m.fs.unit.permeate_side.deltaP_stage.fix(0) + + m.fs.unit.area.fix(membrane_area) + m.fs.unit.width.fix(width) + m.fs.unit.A_comp.fix(A) + m.fs.unit.B_comp.fix(B) + m.fs.unit.reflect_coeff.fix(0.95) + + return m + + @pytest.fixture(scope="class") + def RO_SKK_calculated_frame(self): + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + + m.fs.unit = OsmoticallyAssistedReverseOsmosis1D( + property_package=m.fs.properties, + has_pressure_change=True, + concentration_polarization_type=ConcentrationPolarizationType.calculated, + mass_transfer_coefficient=MassTransferCoefficient.calculated, + pressure_change_type=PressureChangeType.calculated, + transport_model=TransportModel.SKK, + has_full_reporting=True, + ) + + # fully specify system + feed_flow_mass = 5 / 18 + feed_mass_frac_NaCl = 0.075 + feed_pressure = 65e5 + feed_temperature = 273.15 + 25 + membrane_area = 155 + width = 1.1 + A = 1e-12 + B = 7.7e-8 + + feed_cp_mod = 1.05 + permeate_cp_mod = 0.9 + + membrane_pressure_drop = -0.5e-5 + + feed_mass_frac_H2O = 1 - feed_mass_frac_NaCl + m.fs.unit.feed_inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + feed_flow_mass * feed_mass_frac_NaCl + ) + m.fs.unit.feed_inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + feed_flow_mass * feed_mass_frac_H2O + ) + m.fs.unit.feed_inlet.pressure[0].fix(feed_pressure) + m.fs.unit.feed_inlet.temperature[0].fix(feed_temperature) + # m.fs.unit.feed_side.deltaP.fix(membrane_pressure_drop) + + permeate_flow_mass = 0.33 * feed_flow_mass + permeate_mass_frac_NaCl = 0.1 + permeate_mass_frac_H2O = 1 - permeate_mass_frac_NaCl + m.fs.unit.permeate_inlet.flow_mass_phase_comp[0, "Liq", "H2O"].fix( + permeate_flow_mass * permeate_mass_frac_H2O + ) + m.fs.unit.permeate_inlet.flow_mass_phase_comp[0, "Liq", "NaCl"].fix( + permeate_flow_mass * permeate_mass_frac_NaCl + ) + m.fs.unit.permeate_inlet.pressure[0].fix(5e5) + m.fs.unit.permeate_inlet.temperature[0].fix(feed_temperature) + + # m.fs.unit.permeate_side.deltaP.fix(membrane_pressure_drop) + + m.fs.unit.area.fix(membrane_area) + m.fs.unit.width.fix(width) + m.fs.unit.A_comp.fix(A) + m.fs.unit.B_comp.fix(B) + m.fs.unit.reflect_coeff.fix(0.95) + m.fs.unit.structural_parameter.fix(1200e-6) + + m.fs.unit.permeate_side.channel_height.fix(0.002) + m.fs.unit.permeate_side.spacer_porosity.fix(0.97) + m.fs.unit.feed_side.channel_height.fix(0.002) + m.fs.unit.feed_side.spacer_porosity.fix(0.97) + return m + + @pytest.mark.unit + def test_skk_build(self, RO_SKK_frame): + m = RO_SKK_frame + + # test ports + port_lst = ["feed_inlet", "feed_outlet", "permeate_inlet", "permeate_outlet"] + for port_str in port_lst: + port = getattr(m.fs.unit, port_str) + assert isinstance(port, Port) + # number of state variables for NaCl property package + assert len(port.vars) == 3 + + # test feed-side control volume and associated stateblocks + assert isinstance(m.fs.unit.feed_side, MembraneChannel1DBlock) + assert isinstance(m.fs.unit.permeate_side, MembraneChannel1DBlock) + + # test statistics + assert number_variables(m) == 754 + assert number_total_constraints(m) == 684 + assert number_unused_variables(m) == 30 + + @pytest.mark.unit + def test_skk_dof(self, RO_SKK_frame): + m = RO_SKK_frame + assert degrees_of_freedom(m) == 0 + + @pytest.mark.unit + def test_skk_general_dof(self, RO_SKK_calculated_frame): + m = RO_SKK_calculated_frame + assert degrees_of_freedom(m) == 0 + + @pytest.mark.unit + def test_skk_calculate_scaling(self, RO_SKK_frame): + m = RO_SKK_frame + + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e3, index=("Liq", "NaCl") + ) + calculate_scaling_factors(m) + + # check that all variables have scaling factors + unscaled_var_list = list(unscaled_variables_generator(m)) + assert len(unscaled_var_list) == 0 + + for i in badly_scaled_var_generator(m): + print(i[0].name, i[1]) + + @pytest.mark.component + def test_skk_initialize(self, RO_SKK_frame): + initialization_tester(RO_SKK_frame) + + @pytest.mark.component + def test_skk_var_scaling(self, RO_SKK_frame): + m = RO_SKK_frame + badly_scaled_var_lst = list(badly_scaled_var_generator(m)) + [print(i[0], i[1]) for i in badly_scaled_var_lst] + assert badly_scaled_var_lst == [] + + @pytest.mark.component + def test_skk_solve(self, RO_SKK_frame): + m = RO_SKK_frame + results = solver.solve(m) + + # Check for optimal solution + assert_optimal_termination(results) + + @pytest.mark.component + def test_skk_conservation(self, RO_SKK_frame): + m = RO_SKK_frame + b = m.fs.unit + comp_lst = ["NaCl", "H2O"] + + feed_flow_mass_inlet = sum( + b.feed_side.properties[0, 0].flow_mass_phase_comp["Liq", j] + for j in comp_lst + ) + feed_flow_mass_outlet = sum( + b.feed_side.properties[0, 1].flow_mass_phase_comp["Liq", j] + for j in comp_lst + ) + perm_flow_mass_inlet = sum( + b.permeate_side.properties[0, 1].flow_mass_phase_comp["Liq", j] + for j in comp_lst + ) + perm_flow_mass_outlet = sum( + b.permeate_side.properties[0, 0].flow_mass_phase_comp["Liq", j] + for j in comp_lst + ) + + assert ( + abs( + value( + feed_flow_mass_inlet + + perm_flow_mass_inlet + - feed_flow_mass_outlet + - perm_flow_mass_outlet + ) + ) + <= 1e-5 + ) + + assert ( + abs( + value( + feed_flow_mass_inlet + * b.feed_side.properties[0, 0].enth_mass_phase["Liq"] + - feed_flow_mass_outlet + * b.feed_side.properties[0, 1].enth_mass_phase["Liq"] + + perm_flow_mass_inlet + * b.permeate_side.properties[0, 1].enth_mass_phase["Liq"] + - perm_flow_mass_outlet + * b.permeate_side.properties[0, 0].enth_mass_phase["Liq"] + ) + ) + <= 1e-5 + ) + + @pytest.mark.component + def test_skk_solution(self, RO_SKK_frame): + m = RO_SKK_frame + assert pytest.approx(8.5717e-4, rel=1e-3) == value( + m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "H2O"] + ) + assert pytest.approx(9.242e-06, rel=1e-3) == value( + m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "NaCl"] + ) + assert pytest.approx(1.241e-01, rel=1e-3) == value( + m.fs.unit.feed_outlet.flow_mass_phase_comp[0, "Liq", "H2O"] + ) + assert pytest.approx(1.940e-02, rel=1e-3) == value( + m.fs.unit.feed_outlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ) + assert pytest.approx( + value( + m.fs.unit.feed_side.cp_modulus[ + 0, m.fs.unit.difference_elements.first(), "NaCl" + ] + ), + rel=1e-3, + ) == value( + m.fs.unit.feed_side.properties_interface[ + 0, m.fs.unit.difference_elements.first() + ].conc_mass_phase_comp["Liq", "NaCl"] + ) / value( + m.fs.unit.feed_side.properties[ + 0, m.fs.unit.difference_elements.first() + ].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx( + value(m.fs.unit.feed_side.cp_modulus[0, 1, "NaCl"]), rel=1e-3 + ) == value( + m.fs.unit.feed_side.properties_interface[0, 1].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) / value( + m.fs.unit.feed_side.properties[0, 1].conc_mass_phase_comp["Liq", "NaCl"] + ) + + assert pytest.approx( + value( + m.fs.unit.permeate_side.cp_modulus[ + 0, m.fs.unit.difference_elements.first(), "NaCl" + ] + ), + rel=1e-3, + ) == value( + m.fs.unit.permeate_side.properties_interface[ + 0, m.fs.unit.difference_elements.first() + ].conc_mass_phase_comp["Liq", "NaCl"] + ) / value( + m.fs.unit.permeate_side.properties[ + 0, m.fs.unit.difference_elements.first() + ].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx( + value(m.fs.unit.permeate_side.cp_modulus[0, 1.0, "NaCl"]), rel=1e-3 + ) == value( + m.fs.unit.permeate_side.properties_interface[0, 1.0].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) / value( + m.fs.unit.permeate_side.properties[0, 1].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx(0, abs=1e-3) == value(m.fs.unit.feed_side.deltaP_stage[0]) + assert pytest.approx(0, abs=1e-3) == value( + m.fs.unit.permeate_side.deltaP_stage[0] + ) + + @pytest.mark.component + def test_skk_CP_calculation_with_kf_calculation(self, RO_SKK_calculated_frame): + m = RO_SKK_calculated_frame + + # Test units + assert_units_consistent(m.fs.unit) + + # test degrees of freedom + assert degrees_of_freedom(m) == 0 + + # test scaling + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e1, index=("Liq", "H2O") + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", 1e3, index=("Liq", "NaCl") + ) + calculate_scaling_factors(m) + + # check that all variables have scaling factors. + unscaled_var_list = list( + unscaled_variables_generator(m.fs.unit, include_fixed=True) + ) + [print(i) for i in unscaled_var_list] + assert len(unscaled_var_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 + results = solver.solve(m) + + # Check for optimal solution + assert_optimal_termination(results) + + assert ( + m.fs.unit.config.concentration_polarization_type + == ConcentrationPolarizationType.calculated + ) + assert ( + m.fs.unit.config.mass_transfer_coefficient + == MassTransferCoefficient.calculated + ) + + assert number_variables(m) == 937 + assert number_total_constraints(m) == 884 + assert number_unused_variables(m) == 23 + + assert pytest.approx(7.215e-4, rel=1e-3) == value( + m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "H2O"] + ) + assert pytest.approx(8.463e-06, rel=1e-3) == value( + m.fs.unit.flux_mass_phase_comp_avg[0, "Liq", "NaCl"] + ) + assert pytest.approx(0.1451, rel=1e-3) == value( + m.fs.unit.feed_outlet.flow_mass_phase_comp[0, "Liq", "H2O"] + ) + assert pytest.approx(0.01952, rel=1e-3) == value( + m.fs.unit.feed_outlet.flow_mass_phase_comp[0, "Liq", "NaCl"] + ) + + assert pytest.approx(78.8775, rel=1e-3) == value( + m.fs.unit.feed_side.properties[0, 0].conc_mass_phase_comp["Liq", "NaCl"] + ) + x_interface_in = m.fs.unit.length_domain.at(2) + assert pytest.approx(88.360, rel=1e-3) == value( + m.fs.unit.feed_side.properties_interface[ + 0, x_interface_in + ].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx(128.619, rel=1e-3) == value( + m.fs.unit.feed_side.properties[0, 1].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx(132.351, rel=1e-3) == value( + m.fs.unit.feed_side.properties_interface[0, 1.0].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) + assert pytest.approx(107.06, rel=1e-3) == value( + m.fs.unit.permeate_side.properties[0, 1].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx(74.889, rel=1e-3) == value( + m.fs.unit.permeate_side.properties_interface[0, 1].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) + assert pytest.approx(52.882, rel=1e-3) == value( + m.fs.unit.permeate_side.properties[0, 0].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx(27.392, rel=1e-3) == value( + m.fs.unit.permeate_side.properties_interface[ + 0, x_interface_in + ].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx( + value( + m.fs.unit.feed_side.cp_modulus[ + 0, m.fs.unit.difference_elements.first(), "NaCl" + ] + ), + rel=1e-3, + ) == value( + m.fs.unit.feed_side.properties_interface[ + 0, m.fs.unit.difference_elements.first() + ].conc_mass_phase_comp["Liq", "NaCl"] + ) / value( + m.fs.unit.feed_side.properties[ + 0, m.fs.unit.difference_elements.first() + ].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx( + value(m.fs.unit.feed_side.cp_modulus[0, 1, "NaCl"]), rel=1e-3 + ) == value( + m.fs.unit.feed_side.properties_interface[0, 1].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) / value( + m.fs.unit.feed_side.properties[0, 1].conc_mass_phase_comp["Liq", "NaCl"] + ) + + assert pytest.approx( + value( + m.fs.unit.permeate_side.cp_modulus[ + 0, m.fs.unit.difference_elements.first(), "NaCl" + ] + ), + rel=1e-3, + ) == value( + m.fs.unit.permeate_side.properties_interface[ + 0, m.fs.unit.difference_elements.first() + ].conc_mass_phase_comp["Liq", "NaCl"] + ) / value( + m.fs.unit.permeate_side.properties[ + 0, m.fs.unit.difference_elements.first() + ].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx( + value(m.fs.unit.permeate_side.cp_modulus[0, 1.0, "NaCl"]), rel=1e-3 + ) == value( + m.fs.unit.permeate_side.properties_interface[0, 1.0].conc_mass_phase_comp[ + "Liq", "NaCl" + ] + ) / value( + m.fs.unit.permeate_side.properties[0, 1].conc_mass_phase_comp["Liq", "NaCl"] + ) + assert pytest.approx(-197270.178, abs=1e-1) == value( + m.fs.unit.feed_side.deltaP_stage[0] + ) + assert pytest.approx(-109664.455, abs=1e-1) == value( + m.fs.unit.permeate_side.deltaP_stage[0] + ) + @pytest.mark.component def test_CP_calculation_with_kf_fixed(self): """Testing 1D-OARO with ConcentrationPolarizationType.calculated option enabled.