diff --git a/docs/how_to_guides/how_to_use_ui_api.rst b/docs/how_to_guides/how_to_use_ui_api.rst index f7cf6e009c..09f8d1ba80 100644 --- a/docs/how_to_guides/how_to_use_ui_api.rst +++ b/docs/how_to_guides/how_to_use_ui_api.rst @@ -11,22 +11,14 @@ How-to guide for the UI API Overview -------- -There are two distinct intended users for this API: - .. image:: /_static/terminal-icon.png :height: 30px :align: left -Model developers can select which variables to "export" to the UI for each component of the model, and provide extra metadata (display name, description) for them. -For flowsheets, they also should specify how to build and solve the flowsheets. - -.. image:: /_static/menu-icon.png - :height: 22px - :align: left - -User interface developers can "discover" flowsheets for inclusion in the UI, and use the API to serialize and update from serialized data. - -The rest of this page will provide more detail on how to do tasks for each type of user. +This API is intended for model developers who would like to connect their flowsheets to the UI. +Developers can select which variables to "export" to the UI for each component of the model, +and provide extra metadata (display name, description) for them. For flowsheets, they should also +specify how to build and solve the flowsheets. See also: :ref:`the UI flowsheet API reference section `. @@ -44,67 +36,154 @@ Add an interface to your flowsheet In some Python module, define the function ``export_to_ui``, which will look similar to this:: - def export_to_ui(): - return FlowsheetInterface( - do_build=build_flowsheet, - do_export=export_variables, - do_solve=solve_flowsheet, - name="My Flowsheet") - -See :class:`FlowsheetInterface` for details on the arguments. - -User Interface Developers --------------------------- - -.. image:: /_static/menu-icon.png - :height: 22px - :align: left - -.. _howto_api-find: - -Find flowsheets -^^^^^^^^^^^^^^^^ -Use the method :meth:`FlowsheetInterface.find` to get a mapping of module names to functions -that, when called, will create the flowsheet interface:: - - results = fsapi.FlowsheetInterface.find("watertap") - -Note that the returned interface will not create the flowsheet object and export the variables until the ``build`` method is invoked:: - - first_module = list(results.keys())[0] - interface = results[first_module] - # at this point the name and description of the flowsheet are available - interface.build() - # at this point the flowsheet is built and all variables exported - - -.. image:: /_static/menu-icon.png - :height: 22px - :align: left - -.. _howto_api-serialize: - -Serialize flowsheet -^^^^^^^^^^^^^^^^^^^^ -Use the ``dict()`` method to serialize the flowsheet:: - - data = flowsheet.dict() - -.. image:: /_static/menu-icon.png - :height: 22px - :align: left - -.. _howto_api-load: - -Load a serialized flowsheet -^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Use the ``load()`` method to load values from a serialized flowsheet. -This will raise a ``MissingObjectError`` if any of the incoming values are not found in the target flowsheet:: - - try: - flowsheet.load(data) - except fsapi.MissingObjectError as err: - print(f"Error loading data: {err}") - # print contents of list of missing items (key and variable name) - for item in err.missing: - print(f"Missing item: key={item.key}, name={item.name}") + from watertap.ui.fsapi import FlowsheetInterface, FlowsheetCategory + def export_to_ui(): + return FlowsheetInterface( + name="NF-DSPM-DE", + do_export=export_variables, + do_build=build_flowsheet, + do_solve=solve_flowsheet, + get_diagram=get_diagram, + requires_idaes_solver=True, + category=FlowsheetCategory.wastewater, + build_options={ + "Bypass": { + "name": "bypass option", + "display_name": "With Bypass", + "values_allowed": ['false', 'true'], + "value": "false" + } + } + ) + +There are 3 required functions: + +1. ``do_export`` - This function defines the variables that will be displayed on the UI. See example below:: + + def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): + fs = flowsheet + exports.add( + obj=fs.feed.properties[0].flow_vol_phase["Liq"], + name="Volumetric flow rate", + ui_units=pyunits.L / pyunits.hr, + display_units="L/h", + rounding=2, + description="Inlet volumetric flow rate", + is_input=True, + input_category="Feed", + is_output=False, + output_category="Feed", + ) + exports.add( + obj=fs.NF.pump.outlet.pressure[0], + name="NF pump pressure", + ui_units=pyunits.bar, + display_units="bar", + rounding=2, + description="NF pump pressure", + is_input=True, + input_category="NF design", + is_output=True, + output_category="NF design", + ) + exports.add( + obj=fs.NF.product.properties[0].flow_vol_phase["Liq"], + name="NF product volume flow", + ui_units=pyunits.L / pyunits.hr, + display_units="L/h", + rounding=2, + description="NF design", + is_input=False, + input_category="NF design", + is_output=True, + output_category="NF design", + ) + +2. ``do_build`` - This function defines the build function for a flowsheet. See example below:: + + from watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.metab.metab import ( + build, + set_operating_conditions, + initialize_system, + solve, + add_costing, + adjust_default_parameters, + ) + def build_flowsheet(): + # build and solve initial flowsheet + m = build() + + set_operating_conditions(m) + assert_degrees_of_freedom(m, 0) + assert_units_consistent(m) + + initialize_system(m) + + results = solve(m) + assert_optimal_termination(results) + + add_costing(m) + assert_degrees_of_freedom(m, 0) + m.fs.costing.initialize() + + adjust_default_parameters(m) + + results = solve(m) + assert_optimal_termination(results) + return m + + +3. ``do_solve`` - This function defines the solve function for a flowsheet. See example below:: + + from watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.metab.metab import solve + def solve_flowsheet(flowsheet=None): + fs = flowsheet + results = solve(fs) + return results + +Additionally, there are optional parameters to assign a category, provide build options, +and provide a diagram function among others. See additional examples below. + +Build function using build options:: + + def build_flowsheet(build_options=None, **kwargs): + # build and solve initial flowsheet + if build_options is not None: + if build_options["Bypass"].value == "true": #build with bypass + solver = get_solver() + m = nf_with_bypass.build() + nf_with_bypass.initialize(m, solver) + nf_with_bypass.unfix_opt_vars(m) + else: # build without bypass + solver = get_solver() + m = nf.build() + nf.initialize(m, solver) + nf.add_objective(m) + nf.unfix_opt_vars(m) + else: # build without bypass + solver = get_solver() + m = nf.build() + nf.initialize(m, solver) + nf.add_objective(m) + nf.unfix_opt_vars(m) + return m + +Custom diagram function:: + + def get_diagram(build_options): + if build_options["Bypass"].value == "true": + return "nf_with_bypass_ui.png" + else: + return "nf_ui.png" + +Enable UI to discover flowsheet - In order for the UI to discover a flowsheet, an +entrypoint must be defined in setup.py with the path to the export file. For examples, see below:: + + entry_points={ + "watertap.flowsheets": [ + "nf = watertap.examples.flowsheets.nf_dspmde.nf_ui", + "metab = watertap.examples.flowsheets.case_studies.wastewater_resource_recovery.metab.metab_ui", + ] + + +For a complete overview of all arguments, see :class:`FlowsheetInterface`. diff --git a/watertap/examples/flowsheets/RO_with_energy_recovery/RO_with_energy_recovery_ui.py b/watertap/examples/flowsheets/RO_with_energy_recovery/RO_with_energy_recovery_ui.py index e416436f3c..fb1124b1f1 100644 --- a/watertap/examples/flowsheets/RO_with_energy_recovery/RO_with_energy_recovery_ui.py +++ b/watertap/examples/flowsheets/RO_with_energy_recovery/RO_with_energy_recovery_ui.py @@ -30,7 +30,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -373,7 +373,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(erd_type=ERDtype.pump_as_turbine): +def build_flowsheet(erd_type=ERDtype.pump_as_turbine, build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2_ui.py b/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2_ui.py index 003beb816a..1a95aabd6c 100644 --- a/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2_ui.py +++ b/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/BSM2_ui.py @@ -40,7 +40,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): """ Exports the variables to the GUI. """ @@ -2844,7 +2844,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): """ Builds the initial flowsheet. """ diff --git a/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/__init__.py b/watertap/examples/flowsheets/case_studies/full_water_resource_recovery_facility/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/GLSD_anaerobic_digester/GLSD_anaerobic_digestion_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/GLSD_anaerobic_digester/GLSD_anaerobic_digestion_ui.py index 5b4aa79931..a56697fd54 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/GLSD_anaerobic_digester/GLSD_anaerobic_digestion_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/GLSD_anaerobic_digester/GLSD_anaerobic_digestion_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -424,7 +424,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/hrcs_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/hrcs_ui.py index dcb1f06c74..109cfefda0 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/hrcs_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_hrcs/hrcs_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet def _base_curr(x): @@ -760,7 +760,7 @@ def _base_curr(x): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/magprex_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/magprex_ui.py index 5a2d1cb846..0d0a357e9a 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/magprex_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1575_magprex/magprex_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -694,7 +694,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_ui.py index 272c396370..222f507b98 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1595_photothermal_membrane_candoP/amo_1595_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -807,7 +807,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_ui.py index 47815ab341..19dea1a4ef 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/amo_1690/amo_1690_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -906,7 +906,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/biomembrane_filtration_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/biomembrane_filtration_ui.py index 93ac5a5185..2a272340df 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/biomembrane_filtration_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/biomembrane_filtration/biomembrane_filtration_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -657,7 +657,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_desalination_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_desalination_ui.py index 23aee987d4..de63ce8734 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_desalination_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/dye_desalination/dye_desalination_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -720,7 +720,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build(include_pretreatment=False) diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/electrochemical_nutrient_removal_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/electrochemical_nutrient_removal_ui.py index f877ab9f72..77bae9565e 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/electrochemical_nutrient_removal_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/electrochemical_nutrient_removal/electrochemical_nutrient_removal_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -541,7 +541,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_ui.py index 56ba333eba..d0092e9b08 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/groundwater_treatment/groundwater_treatment_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -725,7 +725,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/metab_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/metab_ui.py index fc83603de1..ea2980c9cd 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/metab_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/metab/metab_ui.py @@ -32,7 +32,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -887,7 +887,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_ui.py index f3491efb84..42dab93fb4 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/peracetic_acid_disinfection/peracetic_acid_disinfection_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -462,7 +462,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/suboxic_ASM_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/suboxic_ASM_ui.py index acedd49757..5bb84a0651 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/suboxic_ASM_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/suboxic_activated_sludge_process/suboxic_ASM_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -538,7 +538,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/supercritical_sludge_to_gas_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/supercritical_sludge_to_gas_ui.py index 6a1c84d4a8..d3650ca061 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/supercritical_sludge_to_gas_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/supercritical_sludge_to_gas/supercritical_sludge_to_gas_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -608,7 +608,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/swine_wwt_ui.py b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/swine_wwt_ui.py index 1af64af479..c4a2e412b5 100644 --- a/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/swine_wwt_ui.py +++ b/watertap/examples/flowsheets/case_studies/wastewater_resource_recovery/swine_wwt/swine_wwt_ui.py @@ -31,7 +31,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -1154,7 +1154,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet m = build() diff --git a/watertap/examples/flowsheets/nf_dspmde/nf_ui.py b/watertap/examples/flowsheets/nf_dspmde/nf_ui.py index 12b92a2388..60c1677211 100644 --- a/watertap/examples/flowsheets/nf_dspmde/nf_ui.py +++ b/watertap/examples/flowsheets/nf_dspmde/nf_ui.py @@ -9,8 +9,10 @@ # information, respectively. These files are also available online at the URL # "https://github.com/watertap-org/watertap/" ################################################################################# -from watertap.ui.fsapi import FlowsheetInterface +from watertap.ui.fsapi import FlowsheetInterface, FlowsheetCategory from watertap.examples.flowsheets.nf_dspmde import nf +from watertap.examples.flowsheets.nf_dspmde import nf_with_bypass +from watertap.unit_models.nanofiltration_DSPMDE_0D import ConcentrationPolarizationType from pyomo.environ import units as pyunits from idaes.core.solvers import get_solver @@ -21,11 +23,27 @@ def export_to_ui(): do_export=export_variables, do_build=build_flowsheet, do_solve=solve_flowsheet, + get_diagram=get_diagram, requires_idaes_solver=True, + category=FlowsheetCategory.wastewater, + build_options={ + "Bypass": { + "name": "bypass option", + "display_name": "With Bypass", + "values_allowed": ["false", "true"], + "value": "false", + }, + "ConcentrationPolarization": { + "name": "ConcentrationPolarization", + "display_name": "Concentration Polarization Type", + "values_allowed": ["calculated", "none"], + "value": "calculated", + }, + }, ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -337,6 +355,22 @@ def export_variables(flowsheet=None, exports=None): is_output=True, output_category="Process cost and operating metrics", ) + try: + if build_options["Bypass"].value == "true": + exports.add( + obj=fs.by_pass_splitter.split_fraction[0, "bypass"], + name="NF bypass", + ui_units=pyunits.dimensionless, + display_units="fraction", + rounding=4, + description="Bypass design", + is_input=True, + input_category="Bypass design", + is_output=True, + output_category="Bypass design", + ) + except Exception as e: + print(f"error adding bypass: {e}") for (t, phase, ion), obj in fs.NF.nfUnit.rejection_intrinsic_phase_comp.items(): exports.add( @@ -366,16 +400,48 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet - solver = get_solver() - m = nf.build() - nf.initialize(m, solver) - nf.add_objective(m) - nf.unfix_opt_vars(m) + if build_options is not None: + if build_options["Bypass"].value == "true": # build with bypass + solver = get_solver() + m = nf_with_bypass.build() + concentrationType = build_options["ConcentrationPolarization"].value + # print(f'setting concentration polarization type to {concentrationType}') + m.fs.NF.nfUnit.config.concentration_polarization_type = ( + ConcentrationPolarizationType[concentrationType] + ) + nf_with_bypass.initialize(m, solver) + nf_with_bypass.unfix_opt_vars(m) + nf.add_objective(m) + else: # build without bypass + solver = get_solver() + m = nf.build() + concentrationType = build_options["ConcentrationPolarization"].value + # print(f'setting concentration polarization type to {concentrationType}') + m.fs.NF.nfUnit.config.concentration_polarization_type = ( + ConcentrationPolarizationType[concentrationType] + ) + nf.initialize(m, solver) + nf.add_objective(m) + nf.unfix_opt_vars(m) + else: # build without bypass + solver = get_solver() + m = nf.build() + nf.initialize(m, solver) + nf.add_objective(m) + nf.unfix_opt_vars(m) + return m +def get_diagram(build_options): + if build_options["Bypass"].value == "true": + return "nf_with_bypass_ui.png" + else: + return "nf_ui.png" + + def solve_flowsheet(flowsheet=None): fs = flowsheet solver = get_solver() diff --git a/watertap/examples/flowsheets/nf_dspmde/nf_with_bypass_ui.py b/watertap/examples/flowsheets/nf_dspmde/nf_with_bypass_ui.py index 6234b2b9e9..0c4fec7aaa 100644 --- a/watertap/examples/flowsheets/nf_dspmde/nf_with_bypass_ui.py +++ b/watertap/examples/flowsheets/nf_dspmde/nf_with_bypass_ui.py @@ -29,7 +29,7 @@ def export_to_ui(): ) -def export_variables(flowsheet=None, exports=None): +def export_variables(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet # --- Input data --- # Feed conditions @@ -382,7 +382,7 @@ def export_variables(flowsheet=None, exports=None): ) -def build_flowsheet(): +def build_flowsheet(build_options=None, **kwargs): # build and solve initial flowsheet solver = get_solver() m = nf_with_bypass.build() diff --git a/watertap/ui/fsapi.py b/watertap/ui/fsapi.py index c83a00c76e..6706a2f1b3 100644 --- a/watertap/ui/fsapi.py +++ b/watertap/ui/fsapi.py @@ -19,7 +19,7 @@ import logging from collections import namedtuple from enum import Enum -from typing import Any, Callable, Optional, Dict, Union, TypeVar +from typing import Any, Callable, List, Optional, Dict, Union, TypeVar from types import ModuleType try: @@ -32,9 +32,6 @@ from idaes.core.util.model_statistics import degrees_of_freedom from pydantic import BaseModel, validator, Field import pyomo.environ as pyo -from pyomo.core.expr.numeric_expr import ExpressionBase -from pyomo.core.base.expression import Expression, _ExpressionData -from pyomo.core.base.param import ScalarParam #: Forward-reference to a FlowsheetInterface type, used in #: :meth:`FlowsheetInterface.find` @@ -181,6 +178,41 @@ def set_obj_key_default(cls, v, values): return v +class ModelOption(BaseModel): + """An option for building/running the model.""" + + name: str + display_name: str = None + description: str = None + display_values: List[Any] = [] + values_allowed: List[Any] = [] + value: Any = None + + @validator("display_name", always=True) + @classmethod + def validate_display_name(cls, v, values): + if v is None: + v = values.get("name") + return v + + @validator("description", always=True) + @classmethod + def validate_description(cls, v, values): + if v is None: + v = values.get("display_name") + return v + + @validator("value") + @classmethod + def validate_value(cls, v, values): + allowed = values.get("values_allowed", None) + # allowed = list(allowed.keys()) + if v in allowed: + return v + else: + raise ValueError(f"'value' ({v}) not in allowed values: {allowed}") + + class FlowsheetExport(BaseModel): """A flowsheet and its contained exported model objects.""" @@ -193,6 +225,7 @@ class FlowsheetExport(BaseModel): requires_idaes_solver: bool = False dof: int = 0 sweep_results: Union[None, dict] = {} + build_options: Dict[str, ModelOption] = {} # set name dynamically from object @validator("name", always=True) @@ -272,6 +305,21 @@ def add(self, *args, data: Union[dict, ModelExport] = None, **kwargs) -> object: self.model_objects[key] = model_export return model_export + def add_option(self, name: str, **kwargs) -> ModelOption: + """Add an 'option' to the flowsheet that can be displayed and manipulated + from the UI. + + Constructs a :class:`ModelOption` instance with provided args and adds it to + the dict of options, keyed by its `name`. + + Args: + name: Name of option (internal, for accessing the option) + kwargs: Fields of :class:`ModelOption` + """ + option = ModelOption(name=name, **kwargs) + self.build_options[name] = option + return option + class Actions(str, Enum): """Known actions that can be run. @@ -282,6 +330,14 @@ class Actions(str, Enum): build = "build" solve = "solve" export = "_export" + diagram = "diagram" + + +class FlowsheetCategory(str, Enum): + """Flowsheet Categories""" + + wastewater = "Wasterwater Recovery" + desalination = "Desalination" class FlowsheetInterface: @@ -320,6 +376,8 @@ def __init__( do_build: Callable = None, do_export: Callable = None, do_solve: Callable = None, + get_diagram: Callable = None, + category: FlowsheetCategory = None, custom_do_param_sweep_kwargs: Dict = None, **kwargs, ): @@ -357,6 +415,11 @@ def __init__( self.add_action(getattr(Actions, name), arg) else: raise ValueError(f"'do_{name}' argument is required") + if callable(get_diagram): + self.add_action("diagram", get_diagram) + else: + self.add_action("diagram", None) + self._actions["custom_do_param_sweep_kwargs"] = custom_do_param_sweep_kwargs def build(self, **kwargs): @@ -395,6 +458,20 @@ def solve(self, **kwargs): raise RuntimeError(f"Solving flowsheet: {err}") from err return result + def get_diagram(self, **kwargs): + """Return diagram image name. + + Args: + **kwargs: User-defined values + + Returns: + Return image file name if get_diagram function is callable. Otherwise, return none + """ + if self.get_action(Actions.diagram) is not None: + return self.run_action(Actions.diagram, **kwargs) + else: + return None + def dict(self) -> Dict: """Serialize. @@ -425,44 +502,87 @@ def load(self, data: Dict): # set value in this flowsheet ui_units = dst.ui_units if dst.is_input and not dst.is_readonly: - # create a Var so Pyomo can do the unit conversion for us - tmp = pyo.Var(initialize=src.value, units=ui_units) - tmp.construct() - # Convert units when setting value in the model - dst.obj.value = u.convert(tmp, to_units=u.get_units(dst.obj)) - # Don't convert units when setting the exported value - dst.value = src.value - - dst.obj.fixed = src.fixed - dst.fixed = src.fixed - dst.is_sweep = src.is_sweep - dst.num_samples = src.num_samples - # update bounds - if src.lb is None or src.lb == "": - dst.obj.lb = None - dst.lb = None - else: - tmp = pyo.Var(initialize=src.lb, units=ui_units) - tmp.construct() - dst.obj.lb = pyo.value( - u.convert(tmp, to_units=u.get_units(dst.obj)) - ) - dst.lb = src.lb - if src.ub is None or src.ub == "": - dst.obj.ub = None - dst.ub = None - else: - tmp = pyo.Var(initialize=src.ub, units=ui_units) + # only update if value has changed + if dst.value != src.value: + # print(f'changing value for {key} from {dst.value} to {src.value}') + # create a Var so Pyomo can do the unit conversion for us + tmp = pyo.Var(initialize=src.value, units=ui_units) tmp.construct() - dst.obj.ub = pyo.value( - u.convert(tmp, to_units=u.get_units(dst.obj)) - ) - dst.ub = src.ub + # Convert units when setting value in the model + new_val = pyo.value(u.convert(tmp, to_units=u.get_units(dst.obj))) + # print(f'changing value for {key} from {dst.value} to {new_val}') + dst.obj.set_value(new_val) + # Don't convert units when setting the exported value + dst.value = src.value + + if dst.obj.fixed != src.fixed: + # print(f'changing fixed for {key} from {dst.obj.fixed} to {src.fixed}') + if src.fixed: + dst.obj.fix() + else: + dst.obj.unfix() + dst.fixed = src.fixed + + if dst.is_sweep != src.is_sweep: + dst.is_sweep = src.is_sweep + + if dst.num_samples != src.num_samples: + dst.num_samples = src.num_samples + + # update bounds + if dst.lb != src.lb: + # print(f'changing lb for {key} from {dst.lb} to {src.lb}') + if src.lb is None or src.lb == "": + dst.obj.setlb(None) + dst.lb = None + else: + tmp = pyo.Var(initialize=src.lb, units=ui_units) + tmp.construct() + new_lb = pyo.value( + u.convert(tmp, to_units=u.get_units(dst.obj)) + ) + dst.obj.setlb(new_lb) + dst.lb = src.lb + if dst.ub != src.ub: + # print(f'changing ub for {key} from {dst.ub} to {src.ub}') + if src.ub is None or src.ub == "": + dst.obj.setub(None) + dst.ub = None + else: + tmp = pyo.Var(initialize=src.ub, units=ui_units) + tmp.construct() + new_ub = pyo.value( + u.convert(tmp, to_units=u.get_units(dst.obj)) + ) + # print(f'changing ub for {key} from {dst.obj.ub} to {new_ub}') + dst.obj.setub(new_ub) + dst.ub = src.ub + # update degrees of freedom (dof) self.fs_exp.dof = degrees_of_freedom(self.fs_exp.obj) if missing: raise self.MissingObjectError(missing) + def select_option(self, option_name: str, new_option: str): + """Update flowsheet with selected option. + + Args: + data: The input flowsheet + option_name: Name of selected option + + Returns: + None + """ + + # fs = FlowsheetExport.parse_obj(data) # new instance from data + self.fs_exp.build_options[option_name].value = new_option + + # # get function name from model options + # func_name = self.fs_exp.build_options[option_name].values_allowed[new_option] + + # # add functino name as new build function + # self.add_action("build", func_name) + def add_action(self, action_name: str, action_func: Callable): """Add an action for the flowsheet. @@ -473,7 +593,8 @@ def add_action(self, action_name: str, action_func: Callable): Returns: None """ - + # print(f'ADDING ACTION: {action_name}') + # print(action_func) def action_wrapper(**kwargs): if action_name == Actions.build: # set new model object from return value of build action @@ -485,7 +606,6 @@ def action_wrapper(**kwargs): ) self.fs_exp.obj = action_result.fs self.fs_exp.m = action_result - # [re-]create exports (new model object) if Actions.export not in self._actions: raise KeyError( @@ -497,8 +617,13 @@ def action_wrapper(**kwargs): # clear model_objects dict, since duplicates not allowed self.fs_exp.model_objects.clear() # use get_action() since run_action() will refuse to call it directly - self.get_action(Actions.export)(exports=self.fs_exp) + self.get_action(Actions.export)( + exports=self.fs_exp, build_options=self.fs_exp.build_options + ) result = None + elif action_name == Actions.diagram: + self._actions[action_name] = action_func + return elif self.fs_exp.obj is None: raise RuntimeError( f"Cannot run any flowsheet action (except " @@ -555,16 +680,9 @@ def export_values(self): self.fs_exp.dof = degrees_of_freedom(self.fs_exp.obj) for key, mo in self.fs_exp.model_objects.items(): mo.value = pyo.value(u.convert(mo.obj, to_units=mo.ui_units)) - if not isinstance( - mo.obj, - ( - Expression, - ExpressionBase, - _ExpressionData, - ScalarParam, - type(None), - ), - ): + # print(f'{key} is being set to: {mo.value}') + if hasattr(mo.obj, "bounds"): + # print(f'{key} is being set to: {mo.value} from {mo.obj.value}') if mo.obj.ub is None: mo.ub = mo.obj.ub else: diff --git a/watertap/ui/tests/test_flowsheet_interfaces.py b/watertap/ui/tests/test_flowsheet_interfaces.py index 61c917dff0..71dcc63250 100644 --- a/watertap/ui/tests/test_flowsheet_interfaces.py +++ b/watertap/ui/tests/test_flowsheet_interfaces.py @@ -19,6 +19,7 @@ from pyomo.environ import assert_optimal_termination from ..fsapi import FlowsheetInterface +from ..fsapi import ModelOption @pytest.fixture(scope="class") @@ -91,8 +92,23 @@ def test_solve(self, solve_results): @pytest.mark.parametrize("n_times", [2, 3], ids="{} times".format) def test_roundtrip_with_garbage_collection(fs_interface, n_times): + build_options = { + "Bypass": ModelOption( + name="bypass option", + display_name="With Bypass", + values_allowed=["false", "true"], + value="false", + ), + "ConcentrationPolarization": ModelOption( + name="ConcentrationPolarization", + display_name="Concentration Polarization Type", + values_allowed=["calculated", "none"], + value="calculated", + ), + } + for attempt in range(n_times): - fs_interface.build() + fs_interface.build(build_options=build_options) data = fs_interface.dict() fs_interface.load(data) gc.collect() diff --git a/watertap/ui/tests/test_fsapi.py b/watertap/ui/tests/test_fsapi.py index b8e221d578..dd3cd14a3d 100644 --- a/watertap/ui/tests/test_fsapi.py +++ b/watertap/ui/tests/test_fsapi.py @@ -50,7 +50,7 @@ class SOLVE_STATUS: solver = SOLVE_STATUS -def build_ro(**kwargs): +def build_ro(build_options=None, **kwargs): model = RO.build_flowsheet(erd_type=ERD_TYPE) return model @@ -82,7 +82,7 @@ class OutputCategory: revenue = "Revenue" -def export_to_ui(flowsheet=None, exports=None): +def export_to_ui(flowsheet=None, exports=None, build_options=None, **kwargs): fs = flowsheet exports.add( obj=fs.feed.flow_vol[0], @@ -162,7 +162,7 @@ def test_actions(add_variant: str): v1.value = 1 print(v1.display()) - def fake_build(): + def fake_build(build_options=None, **kwargs): nonlocal built built = True nonlocal m @@ -174,7 +174,7 @@ def fake_solve(flowsheet=None): assert flowsheet == m.fs return SOLVE_RESULT_OK - def fake_export(flowsheet=None, exports=None): + def fake_export(flowsheet=None, exports=None, build_options=None, **kwargs): with pytest.raises(Exception): exports.add(obj=garbage)