From d707439e5110a78cd1a15cec47193ec4fc31ed0c Mon Sep 17 00:00:00 2001 From: avdudchenko <33663878+avdudchenko@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:36:25 -0500 Subject: [PATCH 1/8] added tests and code --- .../specsheet_fitting_tools/__init__.py | 0 .../ro_module_fitter.py | 275 ++++++++++++++++++ .../specsheet_fitting_tools/tests/__init__.py | 0 .../expected_ro_results/BW30 PRO-4040.yaml | 30 ++ .../tests/expected_ro_results/RO_module.yaml | 30 ++ .../tests/expected_ro_results/SW30-4040.yaml | 30 ++ .../tests/test_ro_module_fitter.py | 52 ++++ 7 files changed, 417 insertions(+) create mode 100644 watertap/flowsheets/specsheet_fitting_tools/__init__.py create mode 100644 watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py create mode 100644 watertap/flowsheets/specsheet_fitting_tools/tests/__init__.py create mode 100644 watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml create mode 100644 watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/RO_module.yaml create mode 100644 watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml create mode 100644 watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py diff --git a/watertap/flowsheets/specsheet_fitting_tools/__init__.py b/watertap/flowsheets/specsheet_fitting_tools/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py new file mode 100644 index 0000000000..c8ec6a3fdd --- /dev/null +++ b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py @@ -0,0 +1,275 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +__author__ = "Alexander Dudchenko" + +from pyomo.environ import ( + ConcreteModel, + value, + units as pyunits, + TransformationFactory, + assert_optimal_termination, +) + +from pyomo.network import Arc + +from idaes.core.util.model_statistics import degrees_of_freedom +from idaes.core.util.initialization import propagate_state +from idaes.core import FlowsheetBlock +import idaes.core.util.scaling as iscale + +from watertap.unit_models.reverse_osmosis_1D import ( + ReverseOsmosis1D as RO1D, + PressureChangeType, + MassTransferCoefficient, + ConcentrationPolarizationType, +) +from watertap.core.util.model_diagnostics.infeasible import * +from watertap.property_models import NaCl_T_dep_prop_pack as props +from watertap.core.solvers import get_solver +import yaml +from idaes.models.unit_models import Feed +import os + + +def fit_bw30_4040_to_spec_sheet(save_location=None): + # https://www.dupont.com/content/dam/water/amer/us/en/water/public/documents/en/RO-FilmTec-BW30-PRO-4040-and-BW30-PRO-2540-PDS-45-D03970-en.pdf + return fit_ro_module_to_spec_sheet( + membrane_name="BW30 PRO-4040", + water_production_rate=9.8 * pyunits.m**3 / pyunits.day, + nacl_rejection=99.7 * pyunits.percent, # percent + feed_conc=2000 * pyunits.mg / pyunits.liter, + recovery=0.15, + module_length=1 * pyunits.m, + pressure=225 * pyunits.psi, + membrane_area=7.9 * pyunits.m**2, + channel_height=1 * pyunits.mm, + spacer_porosity=0.95, + temperature=25, # degrees C + save_location=save_location, + ) + + +def fit_sw30_4040_to_spec_sheet(save_location=None): + # https://www.dupont.com/content/dam/water/amer/us/en/water/public/documents/en/RO-FilmTec-SW30-Seawater-PDS-45-D01519-en.pdf + return fit_ro_module_to_spec_sheet( + membrane_name="SW30-4040", + water_production_rate=7.4 * pyunits.m**3 / pyunits.day, + nacl_rejection=99.7 * pyunits.percent, # percent + feed_conc=32000 * pyunits.mg / pyunits.liter, + recovery=0.08, + module_length=1 * pyunits.m, + pressure=55 * pyunits.bar, + membrane_area=7.9 * pyunits.m**2, + channel_height=1 * pyunits.mm, + spacer_porosity=0.95, + temperature=25, # degrees C + save_location=save_location, + ) + + +def fit_ro_module_to_spec_sheet( + membrane_name="RO_module", + water_production_rate=38 * pyunits.m**3 / pyunits.day, + nacl_rejection=99.5 * pyunits.percent, # percent + feed_conc=1500 * pyunits.mg / pyunits.liter, + recovery=0.15, + module_length=1 * pyunits.m, + pressure=150 * pyunits.psi, + membrane_area=40 * pyunits.m**2, + channel_height=1e-3 * pyunits.m, + spacer_porosity=0.95, + temperature=25, # degrees C + save_location=None, + save_to_yaml=True, +): + """Method for fitting RO spec sheet data to 1D RO model to find A and B membrane parameters. Use pyunits to specify units + for each input. + + Args: + membrane_name : str + Name of the membrane module. + water_production_rate : float + Water production rate (volume flow rate) [m3/day]. + nacl_rejection : float + Salt rejection [%]. + feed_conc : float + Feed concentration [mg/L]. + recovery : float + Recovery [%]. + module_length : float + Length of the RO module [m]. + pressure : float + Feed pressure [Pa]. + membrane_area : float + Membrane area [m2]. + channel_height : float + Channel height [m]. + spacer_porosity : float + Spacer porosity [-]. + temperature : float + Temperature [deg C]. + save_location : str + Directory to save the fitted membrane parameters yaml file. + save_to_yaml : bool + + Output: + Dictionary that contains fitted membrane parameters A and B along with other + membrane design parameters. + """ + + solver = get_solver() + + m = ConcreteModel() + m.fs = FlowsheetBlock(dynamic=False) + + m.fs.properties = props.NaClParameterBlock() + # seem feed and ro model + m.fs.feed = Feed(property_package=m.fs.properties) + m.fs.RO = RO1D( + property_package=m.fs.properties, + has_pressure_change=True, + pressure_change_type=PressureChangeType.calculated, + mass_transfer_coefficient=MassTransferCoefficient.calculated, + concentration_polarization_type=ConcentrationPolarizationType.calculated, + transformation_scheme="BACKWARD", + transformation_method="dae.finite_difference", + module_type="spiral_wound", + finite_elements=10, + has_full_reporting=True, + ) + + m.fs.feed_to_ro = Arc(source=m.fs.feed.outlet, destination=m.fs.RO.inlet) + + TransformationFactory("network.expand_arcs").apply_to(m) + + # specify feed conditions + m.fs.feed.properties[0].flow_vol_phase["Liq"].fix(water_production_rate / recovery) + m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].fix(feed_conc) + m.fs.feed.properties[0].temperature.fix(temperature + 273.15) + m.fs.feed.properties[0].pressure.fix(pressure) + + print("Degrees of freedom before initialization: ", degrees_of_freedom(m.fs.feed)) + assert degrees_of_freedom(m.fs.feed) == 0 + # Solve the feed to get mass flows and concentrations through out + result = solver.solve(m.fs.feed) + assert_optimal_termination(result) + propagate_state(m.fs.feed_to_ro) + + # configure RO + m.fs.RO.length.fix(module_length) + iscale.set_scaling_factor(m.fs.RO.length, value(1 / m.fs.RO.length)) + m.fs.RO.area.fix(membrane_area) + iscale.set_scaling_factor(m.fs.RO.area, value(1 / m.fs.RO.area)) + m.fs.RO.feed_side.channel_height.fix(channel_height) + m.fs.RO.feed_side.spacer_porosity.fix(spacer_porosity) + # initial guess for intialization + m.fs.RO.A_comp.fix(4.2e-12) + m.fs.RO.B_comp.fix(3.5e-8) + m.fs.RO.permeate.pressure[0].fix(101325) # 1 atm + + # scale mass flow units + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", + value(1 / m.fs.feed.properties[0].flow_mass_phase_comp["Liq", "H2O"]), + index=("Liq", "H2O"), + ) + m.fs.properties.set_default_scaling( + "flow_mass_phase_comp", + value(1 / m.fs.feed.properties[0].flow_mass_phase_comp["Liq", "NaCl"]), + index=("Liq", "NaCl"), + ) + iscale.calculate_scaling_factors(m) + # initialization guess + m.fs.RO.initialize() + + print("Degrees of freedom before RO box solve: ", degrees_of_freedom(m.fs.feed)) + assert degrees_of_freedom(m) == 0 + results = solver.solve(m, tee=False) + assert_optimal_termination(results) + # now fix recovery and rejection to spec sheet values and unfix A and B to solve for them + m.fs.RO.A_comp.unfix() + m.fs.RO.B_comp.unfix() + m.fs.RO.rejection_phase_comp[0, "Liq", "NaCl"].fix(nacl_rejection) + m.fs.RO.recovery_vol_phase[0.0, "Liq"].fix(recovery) + + print("Degrees of freedom before A/B solve: ", degrees_of_freedom(m.fs.feed)) + assert degrees_of_freedom(m) == 0 + results = solver.solve(m, tee=True) + assert_optimal_termination(results) + print('Fit successfully completed for membrane: "', membrane_name, '"') + membrane_design = { + "A": { + "value": m.fs.RO.A_comp[0, "H2O"].value, + "units": str(m.fs.RO.A_comp[0, "H2O"].get_units()), + }, + "B": { + "value": m.fs.RO.B_comp[0, "NaCl"].value, + "units": str(m.fs.RO.B_comp[0, "NaCl"].get_units()), + }, + "A (LMH/bar)": { + "value": value( + pyunits.convert( + m.fs.RO.A_comp[0, "H2O"], + to_units=pyunits.L / (pyunits.m**2 * pyunits.hr * pyunits.bar), + ) + ), + "units": "LMH/bar", + }, + "B (LMH)": { + "value": value( + pyunits.convert( + m.fs.RO.B_comp[0, "NaCl"], + to_units=pyunits.L / (pyunits.m**2 * pyunits.hr), + ) + ), + "units": "LMH", + }, + "Area": {"value": m.fs.RO.area.value, "units": str(m.fs.RO.area.get_units())}, + "Length": { + "value": m.fs.RO.length.value, + "units": str(m.fs.RO.length.get_units()), + }, + "Porosity": { + "value": m.fs.RO.feed_side.spacer_porosity.value, + "units": str(m.fs.RO.feed_side.spacer_porosity.get_units()), + }, + "Channel_height": { + "value": m.fs.RO.feed_side.channel_height.value, + "units": str(m.fs.RO.feed_side.channel_height.get_units()), + }, + "Rejection_NaCl": { + "value": m.fs.RO.rejection_phase_comp[0, "Liq", "NaCl"].value, + "units": str(m.fs.RO.rejection_phase_comp[0, "Liq", "NaCl"].get_units()), + }, + "Recovery": { + "value": m.fs.RO.recovery_vol_phase[0.0, "Liq"].value, + "units": str(m.fs.RO.recovery_vol_phase[0.0, "Liq"].get_units()), + }, + } + for key, val in membrane_design.items(): + print(f"{key}: {val}") + if save_to_yaml: + save_dir = save_location if save_location is not None else os.getcwd() + os.makedirs(save_dir, exist_ok=True) + + print("Saving membrane design to directory: ", save_dir) + with open(f"{save_dir}/{membrane_name}.yaml", "w") as outfile: + yaml.dump( + membrane_design, outfile, default_flow_style=False, sort_keys=False + ) + return membrane_design + + +if __name__ == "__main__": + # fit_bw30_4040_to_spec_sheet() + fit_sw30_4040_to_spec_sheet() diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/__init__.py b/watertap/flowsheets/specsheet_fitting_tools/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml new file mode 100644 index 0000000000..d66882ba07 --- /dev/null +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml @@ -0,0 +1,30 @@ +A: + value: 1.2034550474185542e-11 + units: m**2*s/kg +B: + value: 2.6526570113177454e-08 + units: m/s +A (LMH/bar): + value: 4.332438170706793 + units: LMH/bar +B (LMH): + value: 0.0954956524074388 + units: LMH +Area: + value: 7.9 + units: m**2 +Length: + value: 1.0 + units: m +Porosity: + value: 0.95 + units: dimensionless +Channel_height: + value: 0.001 + units: m +Rejection_NaCl: + value: 0.997 + units: dimensionless +Recovery: + value: 0.15 + units: dimensionless diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/RO_module.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/RO_module.yaml new file mode 100644 index 0000000000..bb9de98261 --- /dev/null +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/RO_module.yaml @@ -0,0 +1,30 @@ +A: + value: 1.4619966399298787e-11 + units: m**2*s/kg +B: + value: 3.612158122479142e-08 + units: m/s +A (LMH/bar): + value: 5.263187903747561 + units: LMH/bar +B (LMH): + value: 0.1300376924092491 + units: LMH +Area: + value: 40.0 + units: m**2 +Length: + value: 1.0 + units: m +Porosity: + value: 0.95 + units: dimensionless +Channel_height: + value: 0.001 + units: m +Rejection_NaCl: + value: 0.995 + units: dimensionless +Recovery: + value: 0.15 + units: dimensionless diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml new file mode 100644 index 0000000000..865364ce41 --- /dev/null +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml @@ -0,0 +1,30 @@ +A: + value: 5.7215619632904135e-12 + units: m**2*s/kg +B: + value: 2.3863535821430525e-08 + units: m/s +A (LMH/bar): + value: 2.0597623067845476 + units: LMH/bar +B (LMH): + value: 0.08590872895714986 + units: LMH +Area: + value: 7.9 + units: m**2 +Length: + value: 1.0 + units: m +Porosity: + value: 0.95 + units: dimensionless +Channel_height: + value: 0.001 + units: m +Rejection_NaCl: + value: 0.997 + units: dimensionless +Recovery: + value: 0.08 + units: dimensionless diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py b/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py new file mode 100644 index 0000000000..c690ab166d --- /dev/null +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py @@ -0,0 +1,52 @@ +import pytest + +from watertap.flowsheets.specsheet_fitting_tools.ro_module_fitter import ( + fit_bw30_4040_to_spec_sheet, + fit_sw30_4040_to_spec_sheet, + fit_ro_module_to_spec_sheet, +) +import os + +_test_file_path = os.path.dirname(os.path.abspath(__file__)) + +import yaml + + +def load_test_yaml(file_name, remove_file=False): + with open(os.path.join(_test_file_path, file_name), "r") as f: + data = yaml.safe_load(f) + if remove_file: + os.remove(file_name) + return data + + +def validate_dict(dict1, dict2, rel_tol=1e-2): + for key in dict1: + assert pytest.approx(dict1[key]["value"], rel=rel_tol) == dict2[key]["value"] + assert dict1[key]["units"] == dict2[key]["units"] + + +def validate_result(ro_module, result): + expected_data = load_test_yaml( + f"{_test_file_path}/expected_ro_results/{ro_module}.yaml" + ) + validate_dict(result, expected_data) + load_result = load_test_yaml( + f"{_test_file_path}/{ro_module}.yaml", remove_file=True + ) + validate_dict(load_result, expected_data) + + +def test_fit_bw30_4040_to_spec_sheet(): + result = fit_bw30_4040_to_spec_sheet(save_location=_test_file_path) + validate_result("BW30 PRO-4040", result) + + +def test_fit_sw30_4040_to_spec_sheet(): + result = fit_sw30_4040_to_spec_sheet(save_location=_test_file_path) + validate_result("SW30-4040", result) + + +def test_fit_ro_module_to_spec_sheet(): + result = fit_ro_module_to_spec_sheet(save_location=_test_file_path) + validate_result("RO_module", result) From 98b8cc73550f7da9d23e293a94ecc4314d97454a Mon Sep 17 00:00:00 2001 From: avdudchenko <33663878+avdudchenko@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:40:26 -0500 Subject: [PATCH 2/8] Update test_ro_module_fitter.py --- .../tests/test_ro_module_fitter.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py b/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py index c690ab166d..ca70ed79eb 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py @@ -1,3 +1,16 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2024, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +__author__ = "Alexander Dudchenko" import pytest from watertap.flowsheets.specsheet_fitting_tools.ro_module_fitter import ( From 1953beb97d17cfa53161c0c56c7e4f8bcd3f32a4 Mon Sep 17 00:00:00 2001 From: avdudchenko <33663878+avdudchenko@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:50:52 -0500 Subject: [PATCH 3/8] add nitto --- .../ro_module_fitter.py | 47 +++++++++++++++++-- .../expected_ro_results/BW30 PRO-4040.yaml | 10 ++-- .../tests/expected_ro_results/ESPA2-LD.yaml | 30 ++++++++++++ .../tests/expected_ro_results/ESPA4-LD.yaml | 30 ++++++++++++ .../tests/expected_ro_results/SW30-4040.yaml | 10 ++-- .../tests/test_ro_module_fitter.py | 12 +++++ 6 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA2-LD.yaml create mode 100644 watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA4-LD.yaml diff --git a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py index c8ec6a3fdd..cb20fc7493 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py +++ b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py @@ -41,6 +41,42 @@ import os +def fit_nitto_ESPA2_LD(save_location=None): + # https://membranes.com/wp-content/uploads/Documents/Element-Specification-Sheets/RO/ESPA/ESPA2-LD.pdf + return fit_ro_module_to_spec_sheet( + membrane_name="ESPA2-LD", + water_production_rate=37.9 * pyunits.m**3 / pyunits.day, + nacl_rejection=99.6 * pyunits.percent, # percent + feed_conc=1500 * pyunits.mg / pyunits.liter, + recovery=0.15, + module_length=1 * pyunits.m, + pressure=150 * pyunits.psi, + membrane_area=37.2 * pyunits.m**2, + channel_height=0.86 * pyunits.mm, + spacer_porosity=0.9, + temperature=25, # degrees C + save_location=save_location, + ) + + +def fit_nitto_ESPA4_LD(save_location=None): + # https://membranes.com/wp-content/uploads/Documents/Element-Specification-Sheets/RO/ESPA/ESPA4-LD.pdf + return fit_ro_module_to_spec_sheet( + membrane_name="ESPA4-LD", + water_production_rate=45.4 * pyunits.m**3 / pyunits.day, + nacl_rejection=99.2 * pyunits.percent, # percent + feed_conc=500 * pyunits.mg / pyunits.liter, + recovery=0.15, + module_length=1 * pyunits.m, + pressure=100 * pyunits.psi, + membrane_area=37.2 * pyunits.m**2, + channel_height=0.86 * pyunits.mm, + spacer_porosity=0.9, + temperature=25, # degrees C + save_location=save_location, + ) + + def fit_bw30_4040_to_spec_sheet(save_location=None): # https://www.dupont.com/content/dam/water/amer/us/en/water/public/documents/en/RO-FilmTec-BW30-PRO-4040-and-BW30-PRO-2540-PDS-45-D03970-en.pdf return fit_ro_module_to_spec_sheet( @@ -53,7 +89,7 @@ def fit_bw30_4040_to_spec_sheet(save_location=None): pressure=225 * pyunits.psi, membrane_area=7.9 * pyunits.m**2, channel_height=1 * pyunits.mm, - spacer_porosity=0.95, + spacer_porosity=0.9, temperature=25, # degrees C save_location=save_location, ) @@ -71,7 +107,7 @@ def fit_sw30_4040_to_spec_sheet(save_location=None): pressure=55 * pyunits.bar, membrane_area=7.9 * pyunits.m**2, channel_height=1 * pyunits.mm, - spacer_porosity=0.95, + spacer_porosity=0.9, temperature=25, # degrees C save_location=save_location, ) @@ -172,7 +208,7 @@ def fit_ro_module_to_spec_sheet( iscale.set_scaling_factor(m.fs.RO.area, value(1 / m.fs.RO.area)) m.fs.RO.feed_side.channel_height.fix(channel_height) m.fs.RO.feed_side.spacer_porosity.fix(spacer_porosity) - # initial guess for intialization + # initial guess for initialization m.fs.RO.A_comp.fix(4.2e-12) m.fs.RO.B_comp.fix(3.5e-8) m.fs.RO.permeate.pressure[0].fix(101325) # 1 atm @@ -271,5 +307,8 @@ def fit_ro_module_to_spec_sheet( if __name__ == "__main__": - # fit_bw30_4040_to_spec_sheet() fit_sw30_4040_to_spec_sheet() + fit_bw30_4040_to_spec_sheet() + fit_nitto_ESPA2_LD() + fit_nitto_ESPA4_LD() + fit_ro_module_to_spec_sheet() diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml index d66882ba07..85bcf8d046 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml @@ -1,14 +1,14 @@ A: - value: 1.2034550474185542e-11 + value: 1.1925863212636584e-11 units: m**2*s/kg B: - value: 2.6526570113177454e-08 + value: 2.8067682050960293e-08 units: m/s A (LMH/bar): - value: 4.332438170706793 + value: 4.293310756549168 units: LMH/bar B (LMH): - value: 0.0954956524074388 + value: 0.10104365538345703 units: LMH Area: value: 7.9 @@ -17,7 +17,7 @@ Length: value: 1.0 units: m Porosity: - value: 0.95 + value: 0.9 units: dimensionless Channel_height: value: 0.001 diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA2-LD.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA2-LD.yaml new file mode 100644 index 0000000000..1cacaadafd --- /dev/null +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA2-LD.yaml @@ -0,0 +1,30 @@ +A: + value: 1.5554387653894895e-11 + units: m**2*s/kg +B: + value: 3.342102859050665e-08 + units: m/s +A (LMH/bar): + value: 5.599579555402159 + units: LMH/bar +B (LMH): + value: 0.12031570292582391 + units: LMH +Area: + value: 37.2 + units: m**2 +Length: + value: 1.0 + units: m +Porosity: + value: 0.9 + units: dimensionless +Channel_height: + value: 0.00086 + units: m +Rejection_NaCl: + value: 0.996 + units: dimensionless +Recovery: + value: 0.15 + units: dimensionless diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA4-LD.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA4-LD.yaml new file mode 100644 index 0000000000..56a529f486 --- /dev/null +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA4-LD.yaml @@ -0,0 +1,30 @@ +A: + value: 2.7238224837903487e-11 + units: m**2*s/kg +B: + value: 7.789552207642498e-08 + units: m/s +A (LMH/bar): + value: 9.80576094164525 + units: LMH/bar +B (LMH): + value: 0.2804238794751298 + units: LMH +Area: + value: 37.2 + units: m**2 +Length: + value: 1.0 + units: m +Porosity: + value: 0.9 + units: dimensionless +Channel_height: + value: 0.00086 + units: m +Rejection_NaCl: + value: 0.9920000000000001 + units: dimensionless +Recovery: + value: 0.15 + units: dimensionless diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml index 865364ce41..58f0417ca9 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml @@ -1,14 +1,14 @@ A: - value: 5.7215619632904135e-12 + value: 5.3576465683693945e-12 units: m**2*s/kg B: - value: 2.3863535821430525e-08 + value: 2.4780824989918203e-08 units: m/s A (LMH/bar): - value: 2.0597623067845476 + value: 1.928752764612981 units: LMH/bar B (LMH): - value: 0.08590872895714986 + value: 0.0892109699637055 units: LMH Area: value: 7.9 @@ -17,7 +17,7 @@ Length: value: 1.0 units: m Porosity: - value: 0.95 + value: 0.9 units: dimensionless Channel_height: value: 0.001 diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py b/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py index ca70ed79eb..8034fb3536 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/test_ro_module_fitter.py @@ -17,6 +17,8 @@ fit_bw30_4040_to_spec_sheet, fit_sw30_4040_to_spec_sheet, fit_ro_module_to_spec_sheet, + fit_nitto_ESPA2_LD, + fit_nitto_ESPA4_LD, ) import os @@ -50,6 +52,16 @@ def validate_result(ro_module, result): validate_dict(load_result, expected_data) +def test_fit_ESPA2_LD_to_spec_sheet(): + result = fit_nitto_ESPA2_LD(save_location=_test_file_path) + validate_result("ESPA2-LD", result) + + +def test_fit_ESPA4_LD_to_spec_sheet(): + result = fit_nitto_ESPA4_LD(save_location=_test_file_path) + validate_result("ESPA4-LD", result) + + def test_fit_bw30_4040_to_spec_sheet(): result = fit_bw30_4040_to_spec_sheet(save_location=_test_file_path) validate_result("BW30 PRO-4040", result) From f178841e9d0d4c92e84ca6da13cc0a7816fc1b54 Mon Sep 17 00:00:00 2001 From: avdudchenko <33663878+avdudchenko@users.noreply.github.com> Date: Mon, 17 Nov 2025 22:04:24 -0500 Subject: [PATCH 4/8] fix user test --- .../specsheet_fitting_tools/tests/expected_ro_results/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/__init__.py diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/__init__.py b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 50a79892d16c83accb2f8235ab7cf159d7efac59 Mon Sep 17 00:00:00 2001 From: avdudchenko <33663878+avdudchenko@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:42:12 -0500 Subject: [PATCH 5/8] Update watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py Co-authored-by: Adam Atia --- .../specsheet_fitting_tools/ro_module_fitter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py index cb20fc7493..391eae8b92 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py +++ b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py @@ -189,10 +189,10 @@ def fit_ro_module_to_spec_sheet( TransformationFactory("network.expand_arcs").apply_to(m) # specify feed conditions - m.fs.feed.properties[0].flow_vol_phase["Liq"].fix(water_production_rate / recovery) - m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].fix(feed_conc) - m.fs.feed.properties[0].temperature.fix(temperature + 273.15) - m.fs.feed.properties[0].pressure.fix(pressure) + m.fs.RO.feed_side.properties[0,0].flow_vol_phase["Liq"].fix(water_production_rate / recovery) + m.fs.RO.feed_side.properties[0,0].conc_mass_phase_comp["Liq", "NaCl"].fix(feed_conc) + m.fs.RO.feed_side.properties[0,0].temperature.fix(temperature + 273.15) + m.fs.RO.feed_side.properties[0,0].pressure.fix(pressure) print("Degrees of freedom before initialization: ", degrees_of_freedom(m.fs.feed)) assert degrees_of_freedom(m.fs.feed) == 0 From 38a356646dfcf4b00044f9e49d793d792cc4fd88 Mon Sep 17 00:00:00 2001 From: avdudchenko <33663878+avdudchenko@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:42:20 -0500 Subject: [PATCH 6/8] Update watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py Co-authored-by: Adam Atia --- watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py index 391eae8b92..aa81982579 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py +++ b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py @@ -197,7 +197,7 @@ def fit_ro_module_to_spec_sheet( print("Degrees of freedom before initialization: ", degrees_of_freedom(m.fs.feed)) assert degrees_of_freedom(m.fs.feed) == 0 # Solve the feed to get mass flows and concentrations through out - result = solver.solve(m.fs.feed) + result = solver.solve(m.fs.RO.feed_side.properties[0,0]) assert_optimal_termination(result) propagate_state(m.fs.feed_to_ro) From b8e6a9e9a7bedc3b0f783a06b7776f2860544aca Mon Sep 17 00:00:00 2001 From: avdudchenko <33663878+avdudchenko@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:51:58 -0500 Subject: [PATCH 7/8] Update ro_module_fitter.py --- .../ro_module_fitter.py | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py index aa81982579..a8b2057b17 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py +++ b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py @@ -10,7 +10,7 @@ # "https://github.com/watertap-org/watertap/" ################################################################################# -__author__ = "Alexander Dudchenko" +__author__ = "Alexander V. Dudchenko" from pyomo.environ import ( ConcreteModel, @@ -19,11 +19,7 @@ TransformationFactory, assert_optimal_termination, ) - -from pyomo.network import Arc - from idaes.core.util.model_statistics import degrees_of_freedom -from idaes.core.util.initialization import propagate_state from idaes.core import FlowsheetBlock import idaes.core.util.scaling as iscale @@ -37,7 +33,6 @@ from watertap.property_models import NaCl_T_dep_prop_pack as props from watertap.core.solvers import get_solver import yaml -from idaes.models.unit_models import Feed import os @@ -170,7 +165,6 @@ def fit_ro_module_to_spec_sheet( m.fs.properties = props.NaClParameterBlock() # seem feed and ro model - m.fs.feed = Feed(property_package=m.fs.properties) m.fs.RO = RO1D( property_package=m.fs.properties, has_pressure_change=True, @@ -184,23 +178,33 @@ def fit_ro_module_to_spec_sheet( has_full_reporting=True, ) - m.fs.feed_to_ro = Arc(source=m.fs.feed.outlet, destination=m.fs.RO.inlet) + # m.fs.feed_to_ro = Arc(source=m.fs.feed.outlet, destination=m.fs.RO.inlet) TransformationFactory("network.expand_arcs").apply_to(m) # specify feed conditions - m.fs.RO.feed_side.properties[0,0].flow_vol_phase["Liq"].fix(water_production_rate / recovery) - m.fs.RO.feed_side.properties[0,0].conc_mass_phase_comp["Liq", "NaCl"].fix(feed_conc) - m.fs.RO.feed_side.properties[0,0].temperature.fix(temperature + 273.15) - m.fs.RO.feed_side.properties[0,0].pressure.fix(pressure) + m.fs.RO.feed_side.properties[0, 0].flow_vol_phase["Liq"].fix( + water_production_rate / recovery + ) + m.fs.RO.feed_side.properties[0, 0].conc_mass_phase_comp["Liq", "NaCl"].fix( + feed_conc + ) + m.fs.RO.feed_side.properties[0, 0].temperature.fix(temperature + 273.15) + m.fs.RO.feed_side.properties[0, 0].pressure.fix(pressure) - print("Degrees of freedom before initialization: ", degrees_of_freedom(m.fs.feed)) - assert degrees_of_freedom(m.fs.feed) == 0 + print( + "Degrees of freedom before initialization: ", + degrees_of_freedom(m.fs.RO.feed_side.properties[0, 0]), + ) + assert degrees_of_freedom(m.fs.RO.feed_side.properties[0, 0]) == 0 # Solve the feed to get mass flows and concentrations through out - result = solver.solve(m.fs.RO.feed_side.properties[0,0]) + result = solver.solve(m.fs.RO.feed_side.properties[0, 0]) assert_optimal_termination(result) - propagate_state(m.fs.feed_to_ro) + # RO expects flow mass phase comp to be fixed for intialization + m.fs.RO.feed_side.properties[0, 0].flow_vol_phase["Liq"].unfix() + m.fs.RO.feed_side.properties[0, 0].conc_mass_phase_comp["Liq", "NaCl"].unfix() + m.fs.RO.feed_side.properties[0, 0].flow_mass_phase_comp.fix() # configure RO m.fs.RO.length.fix(module_length) iscale.set_scaling_factor(m.fs.RO.length, value(1 / m.fs.RO.length)) @@ -216,19 +220,23 @@ def fit_ro_module_to_spec_sheet( # scale mass flow units m.fs.properties.set_default_scaling( "flow_mass_phase_comp", - value(1 / m.fs.feed.properties[0].flow_mass_phase_comp["Liq", "H2O"]), + value( + 1 / m.fs.RO.feed_side.properties[0, 0].flow_mass_phase_comp["Liq", "H2O"] + ), index=("Liq", "H2O"), ) m.fs.properties.set_default_scaling( "flow_mass_phase_comp", - value(1 / m.fs.feed.properties[0].flow_mass_phase_comp["Liq", "NaCl"]), + value( + 1 / m.fs.RO.feed_side.properties[0, 0].flow_mass_phase_comp["Liq", "NaCl"] + ), index=("Liq", "NaCl"), ) iscale.calculate_scaling_factors(m) # initialization guess m.fs.RO.initialize() - print("Degrees of freedom before RO box solve: ", degrees_of_freedom(m.fs.feed)) + print("Degrees of freedom before RO box solve: ", degrees_of_freedom(m)) assert degrees_of_freedom(m) == 0 results = solver.solve(m, tee=False) assert_optimal_termination(results) @@ -238,7 +246,7 @@ def fit_ro_module_to_spec_sheet( m.fs.RO.rejection_phase_comp[0, "Liq", "NaCl"].fix(nacl_rejection) m.fs.RO.recovery_vol_phase[0.0, "Liq"].fix(recovery) - print("Degrees of freedom before A/B solve: ", degrees_of_freedom(m.fs.feed)) + print("Degrees of freedom before A/B solve: ", degrees_of_freedom(m)) assert degrees_of_freedom(m) == 0 results = solver.solve(m, tee=True) assert_optimal_termination(results) From a48f608d37ae372176d04c5054043e85e15e19fc Mon Sep 17 00:00:00 2001 From: avdudchenko <33663878+avdudchenko@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:36:35 -0500 Subject: [PATCH 8/8] upate porosity --- .../specsheet_fitting_tools/ro_module_fitter.py | 10 +++++----- .../tests/expected_ro_results/BW30 PRO-4040.yaml | 10 +++++----- .../tests/expected_ro_results/ESPA2-LD.yaml | 10 +++++----- .../tests/expected_ro_results/ESPA4-LD.yaml | 10 +++++----- .../tests/expected_ro_results/RO_module.yaml | 10 +++++----- .../tests/expected_ro_results/SW30-4040.yaml | 10 +++++----- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py index a8b2057b17..c9fa096c69 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py +++ b/watertap/flowsheets/specsheet_fitting_tools/ro_module_fitter.py @@ -48,7 +48,7 @@ def fit_nitto_ESPA2_LD(save_location=None): pressure=150 * pyunits.psi, membrane_area=37.2 * pyunits.m**2, channel_height=0.86 * pyunits.mm, - spacer_porosity=0.9, + spacer_porosity=0.85, temperature=25, # degrees C save_location=save_location, ) @@ -66,7 +66,7 @@ def fit_nitto_ESPA4_LD(save_location=None): pressure=100 * pyunits.psi, membrane_area=37.2 * pyunits.m**2, channel_height=0.86 * pyunits.mm, - spacer_porosity=0.9, + spacer_porosity=0.85, temperature=25, # degrees C save_location=save_location, ) @@ -84,7 +84,7 @@ def fit_bw30_4040_to_spec_sheet(save_location=None): pressure=225 * pyunits.psi, membrane_area=7.9 * pyunits.m**2, channel_height=1 * pyunits.mm, - spacer_porosity=0.9, + spacer_porosity=0.85, temperature=25, # degrees C save_location=save_location, ) @@ -102,7 +102,7 @@ def fit_sw30_4040_to_spec_sheet(save_location=None): pressure=55 * pyunits.bar, membrane_area=7.9 * pyunits.m**2, channel_height=1 * pyunits.mm, - spacer_porosity=0.9, + spacer_porosity=0.85, temperature=25, # degrees C save_location=save_location, ) @@ -118,7 +118,7 @@ def fit_ro_module_to_spec_sheet( pressure=150 * pyunits.psi, membrane_area=40 * pyunits.m**2, channel_height=1e-3 * pyunits.m, - spacer_porosity=0.95, + spacer_porosity=0.85, temperature=25, # degrees C save_location=None, save_to_yaml=True, diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml index 85bcf8d046..9c5ff56246 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/BW30 PRO-4040.yaml @@ -1,14 +1,14 @@ A: - value: 1.1925863212636584e-11 + value: 1.1864448156677198e-11 units: m**2*s/kg B: - value: 2.8067682050960293e-08 + value: 2.9373129406392077e-08 units: m/s A (LMH/bar): - value: 4.293310756549168 + value: 4.271201336403789 units: LMH/bar B (LMH): - value: 0.10104365538345703 + value: 0.10574326586301146 units: LMH Area: value: 7.9 @@ -17,7 +17,7 @@ Length: value: 1.0 units: m Porosity: - value: 0.9 + value: 0.85 units: dimensionless Channel_height: value: 0.001 diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA2-LD.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA2-LD.yaml index 1cacaadafd..1a42b713d0 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA2-LD.yaml +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA2-LD.yaml @@ -1,14 +1,14 @@ A: - value: 1.5554387653894895e-11 + value: 1.555235310803282e-11 units: m**2*s/kg B: - value: 3.342102859050665e-08 + value: 3.458945069362014e-08 units: m/s A (LMH/bar): - value: 5.599579555402159 + value: 5.598847118891813 units: LMH/bar B (LMH): - value: 0.12031570292582391 + value: 0.12452202249703247 units: LMH Area: value: 37.2 @@ -17,7 +17,7 @@ Length: value: 1.0 units: m Porosity: - value: 0.9 + value: 0.85 units: dimensionless Channel_height: value: 0.00086 diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA4-LD.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA4-LD.yaml index 56a529f486..6648327558 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA4-LD.yaml +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/ESPA4-LD.yaml @@ -1,14 +1,14 @@ A: - value: 2.7238224837903487e-11 + value: 2.749202913162948e-11 units: m**2*s/kg B: - value: 7.789552207642498e-08 + value: 8.09476140145459e-08 units: m/s A (LMH/bar): - value: 9.80576094164525 + value: 9.897130487386608 units: LMH/bar B (LMH): - value: 0.2804238794751298 + value: 0.2914114104523652 units: LMH Area: value: 37.2 @@ -17,7 +17,7 @@ Length: value: 1.0 units: m Porosity: - value: 0.9 + value: 0.85 units: dimensionless Channel_height: value: 0.00086 diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/RO_module.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/RO_module.yaml index bb9de98261..8e5bb36264 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/RO_module.yaml +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/RO_module.yaml @@ -1,14 +1,14 @@ A: - value: 1.4619966399298787e-11 + value: 1.4441273763939004e-11 units: m**2*s/kg B: - value: 3.612158122479142e-08 + value: 3.9359866018440615e-08 units: m/s A (LMH/bar): - value: 5.263187903747561 + value: 5.198858555018039 units: LMH/bar B (LMH): - value: 0.1300376924092491 + value: 0.1416955176663862 units: LMH Area: value: 40.0 @@ -17,7 +17,7 @@ Length: value: 1.0 units: m Porosity: - value: 0.95 + value: 0.85 units: dimensionless Channel_height: value: 0.001 diff --git a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml index 58f0417ca9..fb697e3971 100644 --- a/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml +++ b/watertap/flowsheets/specsheet_fitting_tools/tests/expected_ro_results/SW30-4040.yaml @@ -1,14 +1,14 @@ A: - value: 5.3576465683693945e-12 + value: 5.113908764712423e-12 units: m**2*s/kg B: - value: 2.4780824989918203e-08 + value: 2.554496834650047e-08 units: m/s A (LMH/bar): - value: 1.928752764612981 + value: 1.8410071552964713 units: LMH/bar B (LMH): - value: 0.0892109699637055 + value: 0.09196188604740167 units: LMH Area: value: 7.9 @@ -17,7 +17,7 @@ Length: value: 1.0 units: m Porosity: - value: 0.9 + value: 0.85 units: dimensionless Channel_height: value: 0.001