diff --git a/.gitignore b/.gitignore
index 501d3256ef..232d6fbb4f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -154,4 +154,100 @@ auto-save-list
.DS_Store
# Example outputs
-output/
\ No newline at end of file
+output/
+watertap/flowsheets/ccro/validation_data/sine_700-900psi_60s_period.csv
+watertap/flowsheets/ccro/validation_data/sine_500-900psi_60s_period.csv
+watertap/flowsheets/ccro/validation_data/sine_500-900psi_10s_period.csv
+watertap/flowsheets/ccro/validation_data/Recovery.csv
+watertap/flowsheets/ccro/validation_data/Pressure and recovery from OC.jpg
+watertap/flowsheets/ccro/validation_data/Outlet Pressure.csv
+watertap/flowsheets/ccro/validation_data/Feed pressure.csv
+watertap/flowsheets/ccro/validation_data/Approx_3hr_15LMH_50Recirc.csv
+watertap/flowsheets/ccro/analysis_scripts/output_old/ccro_brine_sweep_analysisType_ccro_sweep_brackish_recovery_r1.h5
+watertap/flowsheets/ccro/analysis_scripts/output_old/ccro_brine_sweep_analysisType_ccro_sweep_brackish_recovery.h5_30_09-21_05_33_.bak
+watertap/flowsheets/ccro/analysis_scripts/output_old/ccro_brine_sweep_analysisType_ccro_sweep_brackish_recovery.h5_30_09-21_03_40_.bak
+watertap/flowsheets/ccro/analysis_scripts/output_old/ccro_brine_sweep_analysisType_ccro_sweep_brackish_recovery.h5
+watertap/flowsheets/ccro/analysis_scripts/output_old/ccro_brine_sweep_analysisType_ccro_sweep_brackish.h5_30_09-14_09_20_.bak
+watertap/flowsheets/ccro/analysis_scripts/output_old/ccro_brine_sweep_analysisType_ccro_sweep_brackish.h5
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW lcow map True.svg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW lcow map True.jpg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW lcow map True.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW lcow map False.svg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW lcow map False.jpg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW lcow map False.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW flushing efficiency map True.svg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW flushing efficiency map True.jpg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW flushing efficiency map True.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW flushing efficiency map False.svg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW flushing efficiency map False.jpg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW flushing efficiency map False.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW cycle time map True.svg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW cycle time map True.jpg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW cycle time map True.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW cycle time map False.svg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW cycle time map False.jpg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW cycle time map False.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW area map True.svg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW area map True.jpg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW area map True.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW area map False.svg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW area map False.jpg
+watertap/flowsheets/ccro/analysis_scripts/map_figs/BGW area map False.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/validation_water_flux.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/validation_water_flux.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/validation_water_flux.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/validation_retentate_tds.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/validation_retentate_tds.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/validation_retentate_tds.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/validation_permeate_tds.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/validation_permeate_tds.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/validation_permeate_tds.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/Total cycle time.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/Total cycle time.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/Total cycle time.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Total cycle time.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Total cycle time.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Total cycle time.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/SW RO SP.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW RO SP.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW RO SP.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Recycle.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Recycle.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Recycle.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Length.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Length.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Length.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/SW lcow.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW lcow.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW lcow.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Flushing efficiency.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Flushing efficiency.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Flushing efficiency.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Area.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Area.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/SW Area.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/RO SP.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/RO SP.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/RO SP.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/RO residence time.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/Recycle.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/Recycle.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/Recycle.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/Length.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/Length.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/Length.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/lcow.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/lcow.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/lcow.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/Flushing efficiency.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/Flushing efficiency.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/Flushing efficiency.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/ccro_val_retentante_tds.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/ccro_val_retentante_tds.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/ccro_val_retentante_tds.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/ccro_val_feed_pressure.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/ccro_val_feed_pressure.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/ccro_val_feed_pressure.csv.csv
+watertap/flowsheets/ccro/analysis_scripts/figs/Area.svg
+watertap/flowsheets/ccro/analysis_scripts/figs/Area.jpg
+watertap/flowsheets/ccro/analysis_scripts/figs/Area.csv.csv
diff --git a/setup.py b/setup.py
index e9a1233d37..a6c814e819 100644
--- a/setup.py
+++ b/setup.py
@@ -77,6 +77,7 @@
"parameter-sweep >=0.1.0",
"numpy",
"pint<0.25",
+ "psPlotKit @ git+https://github.com/avdudchenko/psPlotKit@V0.21alpha",
],
extras_require={
"testing": [
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/__init__.py b/watertap/flowsheets/lsrro/loopTool_workflow/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/analysis_setup.py b/watertap/flowsheets/lsrro/loopTool_workflow/analysis_setup.py
new file mode 100644
index 0000000000..46ffdce10d
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/analysis_setup.py
@@ -0,0 +1,341 @@
+from parameter_sweep.loop_tool.loop_tool import loopTool, get_working_dir
+from watertap.flowsheets.lsrro import lsrro
+import multiprocessing
+
+from watertap.core.solvers import get_solver
+from watertap.tools.sensitivity_manager import SensitivityManager
+
+##############################################################
+# For loop tool/parameter sweeep interface we need in general, 3 functions:
+# 1) build_function - builds the model and configures it for initialization
+# 2) initialize_function - initializes the model and configures it for optimization
+# 3) optimize_function - optimizes/solves the model
+# 4) (optional) probe_function - probes the model for feasibility or other metrics (e.g. ensures we don't try to solve when recovery is too high)
+
+# Your workflow should go as follows:
+# 1) Build flowsheet/model and define all functions
+# 2) ensure model actually solves using defined functions
+# 3) set up loop tool with yaml file and run analysis
+# 4) use psPlotKit to analyze results from h5 file - unlike in multi_sweep example, we don't need to
+# define expression or outputs manually, all data on flowsheet will be saved and we can do all the
+# analysis in psPlotKit
+##############################################################
+
+
+def build_function(
+ number_of_stages=2,
+ has_CP=True,
+ add_sensitivity=False,
+ target_tds=125,
+ target_recovery=0.5,
+):
+ """Build LSRRO model with optinal arguments, this should only build the model and configure it
+ for initialization
+ Args:
+ number_of_stages (int): number of stages in the LSRRO system
+ A_value (float): membrane water permeability in m/(s*Pa)
+ permeate_quality_limit (float): permeate quality limit in kg solute/kg solution
+ has_CP (bool): whether to include concentration polarization calculations
+ Returns:
+ m (ConcreteModel): LSRRO model"""
+
+ m = lsrro.build(
+ number_of_stages=number_of_stages,
+ has_NaCl_solubility_limit=True,
+ has_calculated_concentration_polarization=has_CP,
+ has_calculated_ro_pressure_drop=True,
+ )
+ lsrro.set_operating_conditions(m)
+
+ # For use in case configuration analysis, we can store target tds and recovery on model
+ m.target_tds = target_tds
+ m.target_recovery = target_recovery
+ if add_sensitivity:
+ add_sensitivity_analysis(m)
+ return m
+
+
+def initialize_model(
+ m,
+ quick_start,
+ A_value,
+ permeate_quality_limit,
+ feed_tds=35,
+ recovery=0.45,
+ **kwargs,
+):
+ """Function to initialize LSRRO model, this function should initialize the model
+ and set it up for optimization or use in the sweep. (e.g The next function we run will solve the model!)
+
+ Args:
+ m (ConcreteModel): LSRRO model to be initialized
+ quick_start (bool): whether to use quick start initialization
+ A_value (float): membrane water permeability in m/(s*Pa)
+ permeate_quality_limit (float): permeate quality limit in kg solute/kg solution
+ **kwargs: additional keyword arguments (These are required for use with paramter sweep and loop tool!)
+ Returns:
+ None
+ """
+ if not quick_start:
+ lsrro.initialize(m)
+ lsrro.solve(m)
+ m.fs.feed.flow_mass_phase_comp.unfix()
+ m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].fix()
+ m.fs.feed.properties[0].flow_vol_phase["Liq"].fix()
+ lsrro.optimize_set_up(
+ m,
+ set_default_bounds_on_module_dimensions=True,
+ A_value=A_value,
+ permeate_quality_limit=permeate_quality_limit,
+ )
+
+ # if we have sense manager on model, activate all sense constraints.
+ if m.fs.find_component("sense_manager"):
+ # aggregate these pressure constraints that are built during optimize_set_up call, which should
+ # really be done during "build call" so we can let sense-manager manage them for
+ # our sensitivity analysis
+ existing_pressure_constraints = []
+ for pump in m.fs.LSRRO_Stages:
+ existing_pressure_constraints.append(
+ m.fs.PrimaryPumps[pump].max_lsrro_pressure_con
+ )
+ existing_pressure_constraints.append(
+ m.fs.BoosterPumps[pump].max_ro_pressure_con
+ )
+ # add these to our sense manager
+ m.fs.sense_manager.sensitivities["pump_pressure"].existing_constraints = (
+ existing_pressure_constraints
+ )
+ m.fs.sense_manager.activate_sensitivities()
+
+ if quick_start:
+ m.fs.water_recovery.fix(0.45)
+ m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].fix(35)
+ m.fs.feed.properties[0].flow_vol_phase["Liq"].fix()
+ lsrro.solve(m, tee=True)
+
+ print("Quick start initialization complete.")
+ if feed_tds != 35 or recovery != 0.45:
+ m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].fix(feed_tds)
+ m.fs.water_recovery.fix(recovery)
+ lsrro.solve(m, tee=True)
+ print(
+ "solved to target tds and recovery of {} g/L and {} %".format(
+ feed_tds, recovery
+ )
+ )
+ if m.target_tds != 35 or m.target_recovery != 0.45:
+ m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].fix(m.target_tds)
+ m.fs.water_recovery.fix(m.target_recovery)
+ lsrro.solve(m, tee=True)
+ print(
+ "solved to target tds and recovery of {} g/L and {} %".format(
+ m.target_tds, m.target_recovery
+ )
+ )
+
+
+def add_sensitivity_analysis(m):
+ """Setup sensitivity manager process block and
+ register sensitivity variables for LSRRO model"""
+ m.fs.sense_manager = SensitivityManager()
+
+ # aggregate all pressures and efficiencies to register for sensitivity analysis
+ pump_pressure = []
+ pump_eff = []
+ pump_eff.append(m.fs.PrimaryPumps[1].efficiency_pump[0.0])
+
+ for pump in m.fs.LSRRO_Stages:
+ pump_pressure.append(m.fs.PrimaryPumps[pump].outlet.pressure[0])
+ pump_eff.append(m.fs.PrimaryPumps[pump].efficiency_pump[0.0])
+
+ for pump in m.fs.LSRRO_Stages:
+ pump_pressure.append(m.fs.BoosterPumps[pump].outlet.pressure[0])
+ pump_eff.append(m.fs.BoosterPumps[pump].efficiency_pump[0.0])
+
+ m.fs.sense_manager.register_sensitivity(
+ sensitivity_name="pump_pressure",
+ model_variables=pump_pressure,
+ sensitivity_type="upper_bound",
+ remove_bounds=True,
+ )
+
+ # aggreagete all LSRRO A values
+ a_values = []
+ for ro in m.fs.LSRRO_Stages:
+ a_values.append(m.fs.ROUnits[ro].A_comp[0, "H2O"])
+
+ # register each one
+ m.fs.sense_manager.register_sensitivity(
+ sensitivity_name="a_value",
+ model_variables=a_values,
+ sensitivity_type="equality",
+ remove_bounds=True,
+ )
+ m.fs.sense_manager.register_sensitivity(
+ sensitivity_name="lsrro_membrane_cost",
+ model_variables=m.fs.costing.reverse_osmosis.high_pressure_membrane_cost,
+ sensitivity_type="equality",
+ remove_bounds=True,
+ )
+ m.fs.sense_manager.register_sensitivity(
+ sensitivity_name="pump_cost",
+ model_variables=m.fs.costing.high_pressure_pump.cost,
+ sensitivity_type="equality",
+ remove_bounds=True,
+ )
+ m.fs.sense_manager.register_sensitivity(
+ sensitivity_name="pump_efficiency",
+ model_variables=pump_eff,
+ sensitivity_type="equality",
+ remove_bounds=True,
+ )
+
+ # fix and scale them
+ m.fs.sense_manager.fix_and_scale()
+
+ # generate templates for our use
+ m.fs.sense_manager.generate_multiplier_yaml_template(
+ "lsrro_sensitivity_template.yaml",
+ multiplier_lb=0.8,
+ multiplier_ub=1.2,
+ )
+ m.fs.sense_manager.generate_absolute_svoi_template(
+ "svoi_sweep_template_absolute.yaml",
+ )
+ # display current sensitivity values
+ m.fs.sense_manager.display_sensitivities()
+
+
+def solve_model(m, **kwargs):
+ """Function to optimize LSRRO model, this function should solve the model after it has been initialized
+ Args:
+ m (ConcreteModel): LSRRO model to be optimized
+ **kwargs: additional keyword arguments (These are required for use with paramter sweep and loop tool!)
+ Returns:
+ result (SolverResults): result of the solver
+ """
+ result = lsrro.solve(m, tee=False)
+ return result
+
+
+def feasibility_test_function(m, **kwargs):
+ """Function to test feasibility of LSRRO model, this function should return True if the model is feasible
+ Args:
+ m (ConcreteModel): LSRRO model to be tested
+ permeate_quality_limit (float): permeate quality limit in kg solute/kg solution
+ **kwargs: additional keyword arguments (These are required for use with paramter sweep and loop tool!)
+ Returns:
+ feasible (bool): True if the model is feasible, False otherwise
+ """
+ # Example feasibility test: check if permeate concentration is below the limit
+
+ solver = get_solver()
+ solver.solve(m.fs.feed)
+ salt = m.fs.feed.properties[0].flow_mass_phase_comp["Liq", "NaCl"].value
+ recovery = m.fs.water_recovery.value
+ water = m.fs.feed.properties[0].flow_mass_phase_comp["Liq", "H2O"].value
+ salt = salt
+ mass_fraction = salt / (water * (1 - recovery) + salt)
+ print(
+ "Testing recovery of ",
+ recovery,
+ " with feed NaCl of ",
+ m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].value,
+ )
+ if mass_fraction > 0.2648:
+ print(
+ "Would you like some LSRRO with your salt?",
+ "Recovery too high for feed NaCl {} and recovery is {}".format(
+ m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].value,
+ recovery,
+ ),
+ )
+ return False
+ else:
+ print(
+ "Treatment possible for feed NaCl {} and recovery is {}".format(
+ m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].value,
+ recovery,
+ )
+ )
+ return True
+
+
+def test_main_functions():
+ # for i in range(5):
+ i = 5
+ m = build_function(number_of_stages=i + 1, add_sensitivity=True)
+ initialize_model(
+ m,
+ quick_start=True,
+ A_value=1.38e-11,
+ permeate_quality_limit=1000.0e-6,
+ )
+ m.fs.feed.properties[0].conc_mass_phase_comp["Liq", "NaCl"].fix(35)
+ m.fs.water_recovery.fix(0.45)
+ feasible = feasibility_test_function(
+ m,
+ )
+ if feasible:
+ m = solve_model(m)
+ print(f"Model with {i+1} stages solved successfully.")
+ else:
+ print(f"Model with {i+1} stages is not feasible.")
+
+
+def stage_sweep_analysis():
+ """Main function to run loop tool analysis on LSRRO model, this will use
+ the lssrro_stage_sweep.yaml file to define the parameter sweep and run loop tool in parallel mode,
+ where each stage will be run on its own thread, allowing to solve all stages in
+ parallel and speed up the analysis.
+ """
+ cpu_count = multiprocessing.cpu_count() - 2
+ if cpu_count > 10:
+ cpu_count = 10
+ print(f"Working in {get_working_dir()}")
+ loopTool(
+ get_working_dir() + "/lsrro_stage_sweep.yaml",
+ build_function=build_function,
+ initialize_function=initialize_model,
+ optimize_function=solve_model,
+ save_name="lsrro_stage_sweep",
+ probe_function=feasibility_test_function,
+ saving_dir=get_working_dir(),
+ number_of_subprocesses=1,
+ num_loop_workers=cpu_count,
+ )
+
+
+def sensitivity_sweep_analysis():
+ """Main function to run loop tool analysis on LSRRO model, this will use
+ the lsrro_sensitivity_sweep.yaml file to define the parameter sweep and run loop tool in parallel mode,
+ where each stage will be run on its own thread, allowing to solve all stages in
+ parallel and speed up the analysis.
+ """
+ cpu_count = multiprocessing.cpu_count() - 2
+ if cpu_count > 10:
+ cpu_count = 10
+ print(f"Working in {get_working_dir()}")
+ loopTool(
+ get_working_dir() + "/lsrro_sensitivity_sweep.yaml",
+ build_function=build_function,
+ initialize_function=initialize_model,
+ optimize_function=solve_model,
+ save_name="lsrro_sensitivity_sweep",
+ probe_function=feasibility_test_function,
+ saving_dir=get_working_dir(),
+ number_of_subprocesses=1,
+ num_loop_workers=cpu_count,
+ )
+
+
+if __name__ == "__main__":
+ if False:
+ ### Use this to verify all our configured functions work properly
+ test_main_functions()
+ elif False:
+ ### Use this to run the full analysis with loop tool
+ stage_sweep_analysis()
+ elif True:
+ sensitivity_sweep_analysis()
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/case_cost_breakdown_plot.py b/watertap/flowsheets/lsrro/loopTool_workflow/case_cost_breakdown_plot.py
new file mode 100644
index 0000000000..be6a7ac2c9
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/case_cost_breakdown_plot.py
@@ -0,0 +1,112 @@
+from psPlotKit.data_manager.ps_data_manager import PsDataManager
+from psPlotKit.data_plotter.ps_break_down_plotter import BreakDownPlotter
+
+if __name__ == "__main__":
+
+ ##################################################################################
+ # Data processing workflow follows plotting or getting cost breakdowns:
+ # 1) Load data from h5 file using PsDataManager
+ # 2) Createa dictionary of cost groups to use for combing costs in breakdown,
+ # this is required, each group should have general name, and unit keys, if they are indexed all indexed units will be combined
+ # 3) Register data keys to get general LCOW and water recovery which we sweeped over for plotting and data reduction
+
+ # 4) Use breakdown plotter to plot cost breakdowns
+ ###################################################################################
+
+ costing_data = PsDataManager(
+ [
+ "output/lsrro_stage_sweep_analysisType_case_sweep.h5",
+ ]
+ )
+
+ device_groups = {
+ "Pumps and ERDs": {
+ "units": ["PrimaryPumps", "BoosterPumps", "EnergyRecoveryDevices"],
+ },
+ "Membranes": {
+ "units": ["ROUnits"],
+ },
+ }
+
+ costing_data.register_data_key("fs.water_recovery", "Water recovery", "%")
+ costing_data.register_data_key("fs.costing.LCOW", "LCOW")
+ costing_data.get_costing(
+ device_groups,
+ default_flow="fs.product.properties[0.0].flow_vol_phase[Liq]",
+ )
+ costing_data.display()
+ costing_data.reduce_data(
+ stack_keys="number_of_stages",
+ data_key="LCOW",
+ reduction_type="min",
+ )
+ cost_breakdowns = {"CAPEX": {}, "OPEX": {"hatch": "//"}}
+ markers_num_stages = {
+ 2: "s",
+ 3: "^",
+ 4: "D",
+ 5: "v",
+ 6: "P",
+ 7: "*",
+ 8: "X",
+ 9: "<",
+ }
+
+ cases = {
+ "case_a": {"xticks": [30, 40, 50, 60, 70, 80]},
+ "case_b": {"xticks": [30, 35, 40, 45, 50]},
+ }
+ for case in cases:
+ costing_data.select_data(("sim_cases", case), True)
+ wr = costing_data.get_selected_data()
+
+ wr.select_data("stacked_data", True)
+ wr = wr.get_selected_data()
+ wr.display()
+ cost_plotter = BreakDownPlotter(
+ wr,
+ save_name="Cost breakdown for {}".format(case),
+ save_folder="figures",
+ )
+ cost_plotter.define_area_groups(
+ [
+ {"Pumps and ERDs": {"label": None, "color": "#d9f0d3"}},
+ {"Membranes": {"label": None, "color": "#a6cee3"}},
+ ]
+ )
+ cost_plotter.define_hatch_groups(cost_breakdowns)
+
+ cost_plotter.plotbreakdown(
+ xdata="Water recovery",
+ ydata=["cost_breakdown", "levelized"],
+ axis_options={
+ "yticks": [0, 2, 4, 6, 8, 10, 12],
+ "xticks": cases[case]["xticks"],
+ },
+ legend_loc="upper left",
+ generate_figure=False,
+ )
+ wrs = costing_data[("stacked_data", ("sim_cases", case), "Water recovery")].data
+ lcow = costing_data[("stacked_data", ("sim_cases", case), "LCOW")].data
+ stages = costing_data[
+ ("stacked_data", ("sim_cases", case), "number_of_stages")
+ ].data
+ plotted_labels = []
+ for i, wr in enumerate(wrs):
+ if stages[i] == stages[i]: # only plot optimal stages
+ if stages[i] not in plotted_labels:
+ label = f"{int(stages[i])} stages"
+ plotted_labels.append(stages[i])
+ else:
+ label = ""
+ cost_plotter.fig.plot_line(
+ [wrs[i]],
+ [lcow[i]],
+ marker=markers_num_stages[stages[i]],
+ label=label,
+ color="black",
+ ls="",
+ markersize=5,
+ )
+ cost_plotter.fig.add_legend()
+ cost_plotter.generate_figure()
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_a - Water recovery vs LCOW.csv b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_a - Water recovery vs LCOW.csv
new file mode 100644
index 0000000000..2bedbca61b
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_a - Water recovery vs LCOW.csv
@@ -0,0 +1,8 @@
+Figure generated with AnalysisWaterTAP tools
+Water recovery (%),2 stages,,3 stages,4 stages,5 stages,6 stages
+30.0,,,,,,
+58.42105263157895,,,,,,
+64.73684210526316,,,,,,
+67.89473684210527,,,,,,
+71.05263157894738,,,,,,
+74.21052631578948,,,,,,
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_a - Water recovery vs LCOW.jpg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_a - Water recovery vs LCOW.jpg
new file mode 100644
index 0000000000..662816436b
Binary files /dev/null and b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_a - Water recovery vs LCOW.jpg differ
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_a - Water recovery vs LCOW.svg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_a - Water recovery vs LCOW.svg
new file mode 100644
index 0000000000..46f5cfd366
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_a - Water recovery vs LCOW.svg
@@ -0,0 +1,681 @@
+
+
+
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_b - Water recovery vs LCOW.csv b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_b - Water recovery vs LCOW.csv
new file mode 100644
index 0000000000..a0448f0968
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_b - Water recovery vs LCOW.csv
@@ -0,0 +1,7 @@
+Figure generated with AnalysisWaterTAP tools
+Water recovery (%),3 stages,4 stages,,5 stages,6 stages
+30.0,,,,,
+33.1578947368421,,,,,
+42.63157894736842,,,,,
+45.78947368421053,,,,,
+48.94736842105264,,,,,
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_b - Water recovery vs LCOW.jpg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_b - Water recovery vs LCOW.jpg
new file mode 100644
index 0000000000..1a7224ebc6
Binary files /dev/null and b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_b - Water recovery vs LCOW.jpg differ
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_b - Water recovery vs LCOW.svg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_b - Water recovery vs LCOW.svg
new file mode 100644
index 0000000000..fa62464d96
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Cost breakdown for case_b - Water recovery vs LCOW.svg
@@ -0,0 +1,551 @@
+
+
+
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map LCOW_map - None None None.csv b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map LCOW_map - None None None.csv
new file mode 100644
index 0000000000..c5c4b07fa4
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map LCOW_map - None None None.csv
@@ -0,0 +1,17 @@
+Figure generated with AnalysisWaterTAP tools
+Map data for LCOW ($/m^3),
+First column is Water recovery (%)
+First row is Feed TDS (g/L)
+Internal data is LCOW ($/m^3)
+,5.0,32.22222,59.44444,86.66667,113.88889,141.11111,168.33333,nan
+30.0,0.40583754073902767,0.7310235125096031,1.0839557418293322,2.4562273363791194,5.580957871974978,10.706729543367295,18.356000654746502,nan
+36.66667,0.3471818776989186,0.6470384003761923,0.9772510253175039,2.4789982869119447,5.683450148289748,10.757413195020266,nan,nan
+43.33333,0.30766489685867165,0.5947345691308974,1.080531272124205,2.726153247675312,5.9944820995510435,11.403121576946026,nan,nan
+50.0,0.2788984747011127,0.5645683178462029,1.1328173383130549,3.0042979592919936,6.685577354074241,nan,nan,nan
+56.66667,0.25756136278961744,0.5532553488534314,1.2673581042428956,3.5265684005668696,nan,nan,nan,nan
+63.33333,0.24194571678620497,0.5621483213061534,1.567865828163832,4.37811953685669,nan,nan,nan,nan
+70.0,0.23149916194892525,0.6057708834638548,2.0387411656871857,nan,nan,nan,nan,nan
+76.66667,0.2271742366951744,0.7433315268721727,2.946485405073493,nan,nan,nan,nan,nan
+83.33333,0.2339964905055815,1.0767770315772758,nan,nan,nan,nan,nan,nan
+90.0,0.2568229623521704,nan,nan,nan,nan,nan,nan,nan
+nan,nan,nan,nan,nan,nan,nan,nan,nan
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map LCOW_map - None None None.jpg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map LCOW_map - None None None.jpg
new file mode 100644
index 0000000000..d68f45e4f7
Binary files /dev/null and b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map LCOW_map - None None None.jpg differ
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map LCOW_map - None None None.svg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map LCOW_map - None None None.svg
new file mode 100644
index 0000000000..0dcfa4e06f
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map LCOW_map - None None None.svg
@@ -0,0 +1,494 @@
+
+
+
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map optimal_stages - None None None.csv b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map optimal_stages - None None None.csv
new file mode 100644
index 0000000000..24d5dc6c67
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map optimal_stages - None None None.csv
@@ -0,0 +1,17 @@
+Figure generated with AnalysisWaterTAP tools
+Map data for Number of Stages (#),
+First column is Water recovery (%)
+First row is Feed TDS (g/L)
+Internal data is Number of Stages (#)
+,5.0,32.22222,59.44444,86.66667,113.88889,141.11111,168.33333,nan
+30.0,2.0,2.0,2.0,2.0,3.0,4.0,6.0,nan
+36.66667,2.0,2.0,2.0,2.0,3.0,5.0,nan,nan
+43.33333,2.0,2.0,2.0,3.0,4.0,6.0,nan,nan
+50.0,2.0,2.0,2.0,3.0,5.0,nan,nan,nan
+56.66667,2.0,2.0,2.0,4.0,nan,nan,nan,nan
+63.33333,2.0,2.0,3.0,5.0,nan,nan,nan,nan
+70.0,2.0,2.0,4.0,nan,nan,nan,nan,nan
+76.66667,2.0,2.0,6.0,nan,nan,nan,nan,nan
+83.33333,2.0,3.0,nan,nan,nan,nan,nan,nan
+90.0,2.0,nan,nan,nan,nan,nan,nan,nan
+nan,nan,nan,nan,nan,nan,nan,nan,nan
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map optimal_stages - None None None.jpg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map optimal_stages - None None None.jpg
new file mode 100644
index 0000000000..dc8bbb5034
Binary files /dev/null and b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map optimal_stages - None None None.jpg differ
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map optimal_stages - None None None.svg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map optimal_stages - None None None.svg
new file mode 100644
index 0000000000..6c364d78d8
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/Map optimal_stages - None None None.svg
@@ -0,0 +1,479 @@
+
+
+
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/svoi_plot.csv b/watertap/flowsheets/lsrro/loopTool_workflow/figures/svoi_plot.csv
new file mode 100644
index 0000000000..b09a022460
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/svoi_plot.csv
@@ -0,0 +1 @@
+Figure generated with AnalysisWaterTAP tools
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/svoi_plot.jpg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/svoi_plot.jpg
new file mode 100644
index 0000000000..e0db1302a4
Binary files /dev/null and b/watertap/flowsheets/lsrro/loopTool_workflow/figures/svoi_plot.jpg differ
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/svoi_plot.svg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/svoi_plot.svg
new file mode 100644
index 0000000000..42c7474bce
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/svoi_plot.svg
@@ -0,0 +1,673 @@
+
+
+
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/tornado_plot.csv b/watertap/flowsheets/lsrro/loopTool_workflow/figures/tornado_plot.csv
new file mode 100644
index 0000000000..cf8edf44fa
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/tornado_plot.csv
@@ -0,0 +1,3 @@
+Figure generated with AnalysisWaterTAP tools
+key,low_val,high_val
+,0,
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/tornado_plot.jpg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/tornado_plot.jpg
new file mode 100644
index 0000000000..79c46cca75
Binary files /dev/null and b/watertap/flowsheets/lsrro/loopTool_workflow/figures/tornado_plot.jpg differ
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/figures/tornado_plot.svg b/watertap/flowsheets/lsrro/loopTool_workflow/figures/tornado_plot.svg
new file mode 100644
index 0000000000..9bef624d62
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/figures/tornado_plot.svg
@@ -0,0 +1,666 @@
+
+
+
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/lsrro_sensitivity_sweep.yaml b/watertap/flowsheets/lsrro/loopTool_workflow/lsrro_sensitivity_sweep.yaml
new file mode 100644
index 0000000000..2998c7be60
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/lsrro_sensitivity_sweep.yaml
@@ -0,0 +1,140 @@
+tornado_sweep:
+ build_defaults:
+ add_sensitivity: true
+ init_defaults:
+ quick_start: true
+ A_value: 1.38e-11
+ permeate_quality_limit: 1000.0e-6
+ build_loop:
+ sim_cases:
+ case_a:
+ target_tds: 65
+ target_recovery: 0.7
+ number_of_stages: 5
+ case_b:
+ target_tds: 125
+ target_recovery: 0.5
+ number_of_stages: 6
+ sweep_param_loop:
+ pump_pressure_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.pump_pressure_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
+ a_value_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.a_value_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
+ lsrro_membrane_cost_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.lsrro_membrane_cost_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
+ pump_cost_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.pump_cost_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
+ pump_efficiency_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.pump_efficiency_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
+
+svoi_sweep:
+ build_defaults:
+ add_sensitivity: true
+ init_defaults:
+ quick_start: true
+ A_value: 1.38e-11
+ permeate_quality_limit: 1000.0e-6
+ build_loop:
+ sim_cases:
+ case_a:
+ target_tds: 65
+ target_recovery: 0.7
+ number_of_stages: 5
+ case_b:
+ target_tds: 125
+ target_recovery: 0.5
+ number_of_stages: 6
+ diff_param_loop:
+ pump_pressure:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.pump_pressure
+ relative_lb: 0.01
+ relative_ub: 0.01
+ nominal_lb: 7500000.0
+ nominal_ub: 12500000.0
+ num_samples: 1
+ a_value:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.a_value
+ relative_lb: 0.01
+ relative_ub: 0.01
+ nominal_lb: 4.2e-12
+ nominal_ub: 4.2e-11
+ num_samples: 1
+ lsrro_membrane_cost:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.lsrro_membrane_cost
+ relative_lb: -0.01
+ relative_ub: -0.01
+ nominal_lb: 30
+ nominal_ub: 50
+ num_samples: 1
+ pump_cost:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.pump_cost
+ relative_lb: -0.01
+ relative_ub: -0.01
+ nominal_lb: 1.0
+ nominal_ub: 1.908
+ num_samples: 1
+ pump_efficiency:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.pump_efficiency
+ relative_lb: 0.01
+ relative_ub: 0.01
+ nominal_lb: 0.75
+ nominal_ub: 0.9
+ num_samples: 1
+ sweep_reference_params:
+ pump_pressure:
+ type: UniformSample
+ param: fs.sense_manager.pump_pressure
+ lower_limit: 7500000.0
+ upper_limit: 12500000.0
+ a_value:
+ type: UniformSample
+ param: fs.sense_manager.a_value
+ lower_limit: 4.2e-12
+ upper_limit: 4.2e-11
+ lsrro_membrane_cost:
+ type: UniformSample
+ param: fs.sense_manager.lsrro_membrane_cost
+ lower_limit: 30
+ upper_limit: 50
+ pump_cost:
+ type: UniformSample
+ param: fs.sense_manager.pump_cost
+ lower_limit: 1.0
+ upper_limit: 1.908
+ pump_efficiency:
+ type: UniformSample
+ param: fs.sense_manager.pump_efficiency
+ lower_limit: 0.75
+ upper_limit: 0.9
+ num_samples: 10
+
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/lsrro_sensitivity_template.yaml b/watertap/flowsheets/lsrro/loopTool_workflow/lsrro_sensitivity_template.yaml
new file mode 100644
index 0000000000..416fb992e6
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/lsrro_sensitivity_template.yaml
@@ -0,0 +1,30 @@
+pump_pressure_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.pump_pressure_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
+a_value_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.a_value_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
+lsrro_membrane_cost_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.lsrro_membrane_cost_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
+pump_cost_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.pump_cost_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
+pump_efficiency_multiplier:
+ type: LinearSample
+ param: fs.sense_manager.pump_efficiency_multiplier
+ lower_limit: 0.8
+ upper_limit: 1.2
+ num_samples: 3
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/lsrro_stage_sweep.yaml b/watertap/flowsheets/lsrro/loopTool_workflow/lsrro_stage_sweep.yaml
new file mode 100644
index 0000000000..d35c0da623
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/lsrro_stage_sweep.yaml
@@ -0,0 +1,61 @@
+map_sweep:
+ init_defaults:
+ quick_start: true
+ A_value: 1.38e-11
+ permeate_quality_limit: 1000.0e-6
+ build_loop:
+ number_of_stages:
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+ sweep_param_loop:
+ recovery_feed_map:
+ feed_concentration:
+ type: LinearSample
+ param: fs.feed.properties[0].conc_mass_phase_comp[Liq, NaCl]
+ lower_limit: 5
+ upper_limit: 250 #0.85
+ num_samples: 10
+ water_recovery:
+ type: LinearSample
+ param: fs.water_recovery
+ lower_limit: 0.3
+ upper_limit: 0.9 #0.85
+ num_samples: 10
+
+
+case_sweep:
+ init_defaults:
+ quick_start: true
+ A_value: 1.38e-11
+ permeate_quality_limit: 1000.0e-6
+ init_loop:
+ sim_cases:
+ case_a:
+ feed_tds: 65
+ recovery: 0.3
+ case_b:
+ feed_tds: 125
+ recovery: 0.3
+ build_loop:
+ number_of_stages:
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+ sweep_param_loop:
+ water_recovery:
+ type: LinearSample
+ param: fs.water_recovery
+ lower_limit: 0.3
+ upper_limit: 0.9 #0.85
+ num_samples: 20
\ No newline at end of file
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/map_plot.py b/watertap/flowsheets/lsrro/loopTool_workflow/map_plot.py
new file mode 100644
index 0000000000..8153e60317
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/map_plot.py
@@ -0,0 +1,91 @@
+from psPlotKit.data_manager.ps_data_manager import PsDataManager
+from psPlotKit.data_plotter.ps_map_plotter import MapPlotter
+import numpy as np
+
+if __name__ == "__main__":
+
+ ##################################################################################
+ # Data processing workflow follows these steps:
+ # 1) Load data from h5 file using PsDataManager
+ # 2) Register data keys for easier access and labeling
+ # 2.a) Register keys you want to import, they will be imported from every simulation in h5 file, you
+ # can also limit to specific simulations using directories option
+ # 3) Useing data manager its possible to reduce data from all simulations
+ # In this example, we simulated across 9 different stages, we cane use reduce_data option
+ # to find cost-optimal nubmer of stages, by specifing that we want to stack all data based on "number_of_stages"
+ # directory, and use LCOW as our minium objective, any data_key can be used.
+ # The reduction operation will create a new set of data called "stacked data" that can be plotted
+ ###################################################################################
+ data_manager = PsDataManager(
+ [
+ "output/lsrro_stage_sweep_analysisType_map_sweep.h5",
+ ]
+ )
+ # register keys of interest
+ # First input is the model key or file key you see in the h5 file,
+ # return key is the key that will be returned to you
+ # unit is desired final unit for conversion upon import
+ # assign_units allows you to assign or force assignment of a unit to data during import, use it with
+ # conversion_factor to define conversion from default unit to assigned unit
+ # common use case water flux, which might be saved with out default units, so you can assign
+ # unit of LMH, and conversion of 3600 to go from kg/m2/s to L/m2/h
+ data_manager.register_data_key(
+ "fs.water_recovery", return_key="Water recovery", units="%"
+ )
+ data_manager.register_data_key(
+ "fs.feed.properties[0].conc_mass_phase_comp[Liq, NaCl]",
+ return_key="Feed TDS",
+ units="g/L",
+ )
+ data_manager.register_data_key("fs.costing.LCOW", return_key="LCOW")
+ data_manager.load_data()
+ data_manager.display()
+ ### reduce data to find optimal number of stages for each condition
+ data_manager.reduce_data(
+ stack_keys="number_of_stages", data_key="LCOW", reduction_type="min"
+ )
+ data_manager.display()
+
+ # We can select specific data sub sets of data to make it easier to work, and also
+ # ensure we don't accidently plot wrond data
+ data_manager.select_data("stacked_data", True)
+ # generate working data
+ wr = data_manager.get_selected_data()
+ wr.display()
+
+ # WE can sue MapPlotter tool to create our maps for cost and optimal number of stages
+ # This can work really for any variable of interest.
+
+ mp = MapPlotter(wr, save_folder="figures", save_name=f"LCOW_map")
+ mp.plot_map(
+ "stacked_data",
+ ydata="Water recovery",
+ xdata="Feed TDS",
+ zdata="LCOW",
+ zlevels=[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
+ axis_options={
+ "ylabel": "Water recovery (%)",
+ "xlabel": "Feed TDS (g/L)",
+ "xticklabels": [5, 50, 100, 150, 200],
+ "yticklabels": [30, 40, 50, 60, 70, 80, 90],
+ "zlabel": f"LCOW ($\$$/m$^3$)",
+ "zticks": [0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
+ },
+ )
+ mp = MapPlotter(data_manager, save_folder="figures", save_name=f"optimal_stages")
+ mp.plot_map(
+ "stacked_data",
+ ydata="Water recovery",
+ xdata="Feed TDS",
+ zdata="number_of_stages",
+ zlevels=np.arange(0.0, 5.5, 0.5),
+ axis_options={
+ "ylabel": "Water recovery (%)",
+ "xlabel": "Feed TDS (g/L)",
+ "xticklabels": [5, 50, 100, 150, 200, 250],
+ "yticklabels": [30, 40, 50, 60, 70, 80, 90],
+ "yticklabels": [30, 40, 60, 70, 80, 90],
+ "zlabel": f"Number of Stages (#)",
+ "zticks": [2, 3, 4, 5, 6, 7, 8, 9],
+ },
+ )
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/readme.md b/watertap/flowsheets/lsrro/loopTool_workflow/readme.md
new file mode 100644
index 0000000000..a335c4476c
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/readme.md
@@ -0,0 +1,41 @@
+This is an example workflow for analysis using loop tool and psPlotKit.
+
+The workflow is split into 4 steps:
+
+ 1) Setup flowsheet
+ 2) Configure loop tool and analysis .yaml files
+ 3) Run analysis and generate output .h5 files
+ 4) Process data and generate results using psPlotKit
+
+In this workflow, the loop tool and .yaml files create a tractable way of running complex analysis while leveraging advanced capabilities of parameter-sweep tool and .h5 storage format.
+
+Loop tool allows you to configure analysis that run distinct analysis under consistent assumptions and configurations. Examples could be:
+
+ 1) Running model across different designs (number of stages in LSRRO, or different pre-treatment options of flowsheet designs)
+
+ 2) Running specific cases (Feed water compositions, or process designs options)
+
+The loop tool stores all data in a single .h5 file per simulation configuration. For example in a multi-stage analysis all stage results would be stored in the h5 file, in addition, every Variable, Parameter, and Expression value created during the build step will be stored in the file, so you can access all model data and results after simulation/optimization is ran.
+
+The loop tool allows you to run parallel simulations, accelerating analysis. This can be accomplished via two pathways:
+
+ 1) Set number_of_subprocesses > 1 to run parameter sweep simulations in parallel, this is useful when running large maps for a single case. Each processor will create a copy of the model and solve it for each parameter sweep options.
+
+ 2) Set num_loop_workers >1 to run each case in its own thread. This is useful if running on local machine and solving for many flowsheet designs/options (such as number_of_stages), in this case each scenario will be run in its own thread, and sequentially execute the requested parameter sweep.
+
+Finally, you can use psPlotKit to access and process the generated .h5 files and generate figures of interests such as maps and cost breakdowns.
+
+To follow the example workflow, go through following steps:
+
+ 1) Inspect analysis_setup.py - shows how to setup lsrro flowsheet for use with looptool and executes the analysis
+
+ 2) Inspect lssro_stage_sweep.yaml - shows how to configure map and case sweeps across different number of stages
+
+ 3) Execute analysis_setup.py - this will run the simulations and generate an output folder that will contain two files:
+ 3.a) lsrro_stage_sweep_analysisType_map_sweep.h5 - contains results for map sweep
+ 3.b) lsrro_stage_sweep_analysisType_case_sweep.h5 - contains results for case sweep
+ 4) Inspect and execute map_plot.py - uses psPlotKit to plot cost for cost optimal LSRRO design and optimal number of stages
+ 5) Inspect and execute case_cost_breakdown_plot.py - uses psPlotKit to plot cost breakdown for two feed cases across water recoveries.
+
+The above 3 python files contain additional details on the workflows and methods.
+
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/svoi_plot.py b/watertap/flowsheets/lsrro/loopTool_workflow/svoi_plot.py
new file mode 100644
index 0000000000..1127b1bec9
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/svoi_plot.py
@@ -0,0 +1,174 @@
+from psPlotKit.data_manager.ps_data_manager import PsDataManager
+from psPlotKit.data_plotter.fig_generator import figureGenerator
+
+
+if __name__ == "__main__":
+ ##################################################################################
+ # Data processing workflow follows plotting or tornda plot:
+ # 1) Load data from h5 file using PsDataManager
+ # 2) Create a dictionary of baseline values and their names for import
+ # this is required, each group should have general name, and unit keys, if they are indexed all indexed units will be combined
+ # 3) Register data keys for sensitivities
+ # 4) Normalize data (E.g. get change in LCOW)
+ # 5) Build bar plot useinf FigureGenerator
+ ###################################################################################
+
+ data_manager = PsDataManager(
+ [
+ "output/lsrro_sensitivity_sweep_analysisType_svoi_sweep.h5",
+ ]
+ )
+ data_manager.register_data_key("fs.water_recovery", "Water recovery", "%")
+ data_manager.register_data_key(
+ "fs.feed.feed.properties[0].conc_mass_phase_comp[Liq, TDS]", "Feed TDS", "g/L"
+ )
+ data_manager.register_data_key("fs.costing.LCOW", "LCOW")
+
+ # This is for SVOI Specifically
+ data_manager.register_data_key("nominal_idx", "nominal_idx")
+ data_manager.register_data_key("differential_idx", "differential_idx")
+
+ # Dict of registering keys
+ sensitivity_baselines = {
+ "Max. pump pressure": {
+ "directory": "pump_pressure",
+ },
+ "Water permeability": {
+ "directory": "a_value",
+ },
+ "LSRRO mem. cost": {
+ "directory": "lsrro_membrane_cost",
+ },
+ "Pump cost": {
+ "directory": "pump_cost",
+ },
+ "Pump efficiency": {
+ "directory": "pump_efficiency",
+ },
+ }
+
+ for sense_name, sense_info in sensitivity_baselines.items():
+ data_manager.register_data_key(
+ f"fs.sense_manager.{sense_info['directory']}",
+ sense_name,
+ directories=sense_info["directory"],
+ )
+
+ data_manager.load_data()
+ data_manager.display()
+
+ # define SVOI function do calculatin,
+ # We need to calcualte chnage in LCOW between base optimziation (referneced by nominal_idx), and
+ # simulation with improvment (differential_idx)
+ # in our SVOI setup file we are doing 1 percentile step, as such change in LCOW is
+ # the percent change in LCOW/percentile improvment in parameter
+ def svoi_calc(cost, nominal_idx, diff_idx):
+ nominal_cost = cost[nominal_idx == nominal_idx]
+ diff_cost = cost[diff_idx == diff_idx]
+
+ # invert it so its positive (LCOW improvement!)
+ cost_change_percent = -1 * (diff_cost - nominal_cost) / nominal_cost * 100
+ # assert False
+ return cost_change_percent
+
+ treatment_scenarios = {
+ "case_a": {
+ "label": "Case A (65 g/L TDS to 70% WR)",
+ "color": "#7db6df",
+ },
+ "case_b": {
+ "label": "Case B (125 g/L TDS to 50% WR)",
+ "color": "#ff5d5d",
+ },
+ }
+
+ # we need to now run calculation on SVOI, this can be done using
+ # data manager, by creating a dictionary that conntains the directories to be passed into
+ # our function, and then calling eval_function on data manager
+ # it will pull out the data for each directory and directly pass it into the function
+ for case in treatment_scenarios:
+ for sense in sensitivity_baselines:
+ voi_dict = {
+ "cost": (
+ ("sim_cases", case),
+ sensitivity_baselines[sense]["directory"],
+ "LCOW",
+ ),
+ "nominal_idx": (
+ ("sim_cases", case),
+ sensitivity_baselines[sense]["directory"],
+ "nominal_idx",
+ ),
+ "diff_idx": (
+ ("sim_cases", case),
+ sensitivity_baselines[sense]["directory"],
+ "differential_idx",
+ ),
+ }
+ data_manager.eval_function(
+ (("sim_cases", case), sensitivity_baselines[sense]["directory"]),
+ "voi",
+ svoi_calc,
+ voi_dict,
+ )
+ data_manager[
+ ("sim_cases", case),
+ sensitivity_baselines[sense]["directory"],
+ "voi",
+ ].display()
+
+ data_manager.display()
+
+ fig = figureGenerator()
+ fig.init_figure()
+
+ for j, sense in enumerate(sensitivity_baselines):
+ for i, (case_key, case_value) in enumerate(treatment_scenarios.items()):
+ pos = j + i * 0.4 - 0.2
+ voi_data = data_manager[
+ ("sim_cases", case_key),
+ sensitivity_baselines[sense]["directory"],
+ "voi",
+ ]
+ fig.plot_box(
+ pos,
+ voi_data.get_data(exclude_nan_values=True),
+ color=case_value["color"],
+ vertical=False,
+ width=0.4,
+ )
+ if j % 2 == 0:
+ fig.plot_area(
+ [-0.6, -0.6],
+ [pos - 0.3, pos + 0.3],
+ x2data=[1, 1],
+ color="#d6d6d6b2",
+ edgecolor=None,
+ zorder=-10,
+ lw=1,
+ clip_on=False,
+ )
+ for case, case_value in treatment_scenarios.items():
+ fig.plot_bar(
+ [-10],
+ [0],
+ hatch="",
+ color=case_value["color"],
+ label=case_value["label"],
+ )
+ fig.set_axis(
+ xlims=[0, 1],
+ xlabel="SVOI (%$_{\Delta LCOW}$ / %$_{\Delta performance}$)",
+ xticks=[0, 0.2, 0.4, 0.6, 0.8, 1],
+ )
+ fig.set_axis_ticklabels(
+ yticks=list(range(len(sensitivity_baselines))),
+ ylims=[
+ len(sensitivity_baselines) - 0.5,
+ -0.5,
+ ],
+ yticklabels=[key for key, label in sensitivity_baselines.items()],
+ )
+ fig.add_legend(loc="upper center", bbox_to_anchor=[0.5, 1.2])
+ fig.save("figures", "svoi_plot")
+ fig.show()
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/svoi_sweep_template_absolute.yaml b/watertap/flowsheets/lsrro/loopTool_workflow/svoi_sweep_template_absolute.yaml
new file mode 100644
index 0000000000..7a7e82a8b9
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/svoi_sweep_template_absolute.yaml
@@ -0,0 +1,72 @@
+diff_param_loop:
+ pump_pressure:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.pump_pressure
+ relative_lb: -0.01
+ relative_ub: -0.01
+ nominal_lb: 7500000.0
+ nominal_ub: 7500000.0
+ num_samples: 1
+ a_value:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.a_value
+ relative_lb: -0.01
+ relative_ub: -0.01
+ nominal_lb: 4.2e-12
+ nominal_ub: 4.2e-12
+ num_samples: 1
+ lsrro_membrane_cost:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.lsrro_membrane_cost
+ relative_lb: -0.01
+ relative_ub: -0.01
+ nominal_lb: 50
+ nominal_ub: 50
+ num_samples: 1
+ pump_cost:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.pump_cost
+ relative_lb: -0.01
+ relative_ub: -0.01
+ nominal_lb: 1.908
+ nominal_ub: 1.908
+ num_samples: 1
+ pump_efficiency:
+ diff_mode: percentile
+ diff_sample_type: UniformSample
+ param: fs.sense_manager.pump_efficiency
+ relative_lb: -0.01
+ relative_ub: -0.01
+ nominal_lb: 0.75
+ nominal_ub: 0.75
+ num_samples: 1
+ sweep_reference_params:
+ pump_pressure:
+ type: UniformSample
+ param: fs.sense_manager.pump_pressure
+ lower_limit: 7500000.0
+ upper_limit: 7500000.0
+ a_value:
+ type: UniformSample
+ param: fs.sense_manager.a_value
+ lower_limit: 4.2e-12
+ upper_limit: 4.2e-12
+ lsrro_membrane_cost:
+ type: UniformSample
+ param: fs.sense_manager.lsrro_membrane_cost
+ lower_limit: 50
+ upper_limit: 50
+ pump_cost:
+ type: UniformSample
+ param: fs.sense_manager.pump_cost
+ lower_limit: 1.908
+ upper_limit: 1.908
+ pump_efficiency:
+ type: UniformSample
+ param: fs.sense_manager.pump_efficiency
+ lower_limit: 0.75
+ upper_limit: 0.75
diff --git a/watertap/flowsheets/lsrro/loopTool_workflow/tornado_plot.py b/watertap/flowsheets/lsrro/loopTool_workflow/tornado_plot.py
new file mode 100644
index 0000000000..74b8136867
--- /dev/null
+++ b/watertap/flowsheets/lsrro/loopTool_workflow/tornado_plot.py
@@ -0,0 +1,180 @@
+from psPlotKit.data_manager.ps_data_manager import PsDataManager
+from psPlotKit.data_plotter.fig_generator import figureGenerator
+
+
+if __name__ == "__main__":
+ ##################################################################################
+ # Data processing workflow follows plotting or tornda plot:
+ # 1) Load data from h5 file using PsDataManager
+ # 2) Create a dictionary of baseline values and their names for import
+ # this is required, each group should have general name, and unit keys, if they are indexed all indexed units will be combined
+ # 3) Register data keys for sensitivities
+ # 4) Normalize data (E.g. get change in LCOW)
+ # 5) Build bar plot useinf FigureGenerator
+ ###################################################################################
+
+ data_manager = PsDataManager(
+ [
+ "output/lsrro_sensitivity_sweep_analysisType_tornado_sweep.h5",
+ ]
+ )
+ data_manager.register_data_key("fs.water_recovery", "Water recovery", "%")
+ data_manager.register_data_key(
+ "fs.feed.feed.properties[0].conc_mass_phase_comp[Liq, TDS]", "Feed TDS", "g/L"
+ )
+ data_manager.register_data_key("fs.costing.LCOW", "LCOW")
+
+ # the baseline_value here is 1, but if we are using absolute value (e.g. 85 bar, then we would use this for normalizations)
+ sensitivity_baselines = {
+ "Max. pump pressure": {
+ "baseline_val": 1,
+ "directory": "pump_pressure_multiplier",
+ },
+ "Water permeability": {
+ "baseline_val": 1,
+ "directory": "a_value_multiplier",
+ },
+ "LSRRO mem. cost": {
+ "baseline_val": 1,
+ "directory": "lsrro_membrane_cost_multiplier",
+ },
+ "Pump cost": {
+ "baseline_val": 1,
+ "directory": "pump_cost_multiplier",
+ },
+ "Pump efficiency": {
+ "baseline_val": 1,
+ "directory": "pump_efficiency_multiplier",
+ },
+ }
+ _sens_base = {}
+ for sense_name, sense_info in sensitivity_baselines.items():
+ data_manager.register_data_key(
+ f"fs.sense_manager.{sense_info['directory']}",
+ sense_name,
+ directories=sense_info["directory"],
+ )
+ _sens_base[sense_name] = sense_info["baseline_val"]
+
+ data_manager.load_data()
+ data_manager.display()
+
+ # This will normalize the data, it will take the keys in the dictory, find specified
+ # sensetivity var, and "center point" which is the baseline value, and calculate change of LCOW
+ # relative from center point.
+ # e.g. if you do mutlplier then Max. pump pressure will be 0.8, 1, and 1.2, this will find LCOW a 1 and use that
+ # to get change for other values.
+ data_manager.normalize_data(base_value_dict=_sens_base, related_keys="LCOW")
+ data_manager.display()
+
+ treatment_scenarios = {
+ "case_a": {
+ "label": "Case A (65 g/L TDS to 70% WR)",
+ "color": "#7db6df",
+ },
+ "case_b": {
+ "label": "Case B (125 g/L TDS to 50% WR)",
+ "color": "#ff5d5d",
+ },
+ }
+ fig = figureGenerator()
+ fig.init_figure()
+
+ for j, sense in enumerate(sensitivity_baselines):
+ for i, (case_key, case_value) in enumerate(treatment_scenarios.items()):
+ pos = j + i * 0.4 - 0.2
+ _sense_data = data_manager[
+ (
+ ("sim_cases", case_key),
+ sensitivity_baselines[sense]["directory"],
+ "LCOW",
+ )
+ ]
+ sense_data = data_manager[
+ (
+ ("sim_cases", case_key),
+ sensitivity_baselines[sense]["directory"],
+ "normalized_data",
+ "LCOW",
+ )
+ ]
+ sense_steps = data_manager[
+ (
+ ("sim_cases", case_key),
+ sensitivity_baselines[sense]["directory"],
+ "normalized_data",
+ sense,
+ )
+ ]
+ print(sense, case_key, sense_data.data, sense_steps.data, _sense_data.data)
+
+ # We know first value is [0], center point is index [1] and high value is [2]
+ fig.plot_bar(
+ [pos],
+ [sense_data.data[0]],
+ bottom=0,
+ hatch="///",
+ color=case_value["color"],
+ vertical=False,
+ width=0.4,
+ )
+ fig.plot_bar(
+ [pos],
+ [sense_data.data[2]],
+ bottom=0,
+ hatch="",
+ color=case_value["color"],
+ vertical=False,
+ width=0.4,
+ )
+ print(j, pos)
+ if j % 2 == 0:
+ fig.plot_area(
+ [-120, -120],
+ [pos - 0.3, pos + 0.3],
+ x2data=[60, 60],
+ color="#d6d6d6b2",
+ edgecolor=None,
+ zorder=-10,
+ lw=1,
+ clip_on=False,
+ )
+ for case, case_value in treatment_scenarios.items():
+ fig.plot_bar(
+ [-10],
+ [0],
+ hatch="",
+ color=case_value["color"],
+ label=case_value["label"],
+ )
+
+ fig.plot_bar(
+ [-10],
+ [0],
+ hatch="///",
+ color="white",
+ label="-20%",
+ )
+ fig.plot_bar(
+ [-10],
+ [0],
+ hatch="",
+ color="white",
+ label="+20%",
+ )
+ fig.set_axis(
+ xlims=[-30, 60],
+ xlabel="Change in LCOW (%)",
+ xticks=[-30, -20, -10, 0, 10, 20, 30, 40, 50, 60],
+ )
+ fig.set_axis_ticklabels(
+ yticks=list(range(len(sensitivity_baselines))),
+ ylims=[
+ len(sensitivity_baselines) - 0.5,
+ -0.5,
+ ],
+ yticklabels=[key for key, label in sensitivity_baselines.items()],
+ )
+ fig.add_legend(loc="upper center", bbox_to_anchor=[0.5, 1.3])
+ fig.save("figures", "tornado_plot")
+ fig.show()
diff --git a/watertap/tools/sensitivity_manager.py b/watertap/tools/sensitivity_manager.py
new file mode 100644
index 0000000000..bf4ba9094e
--- /dev/null
+++ b/watertap/tools/sensitivity_manager.py
@@ -0,0 +1,410 @@
+from pyomo.environ import (
+ Var,
+ value,
+ Constraint,
+ units as pyunits,
+)
+from idaes.core import (
+ ProcessBlockData,
+ declare_process_block_class,
+)
+
+import idaes.core.util.scaling as iscale
+import yaml
+
+
+class SensitivityType:
+ equality = "equality"
+ upper_bound = "upper_bound"
+ lower_bound = "lower_bound"
+
+
+class SensitivityData:
+ """class for storing sensitivity data"""
+
+ def __init__(
+ self,
+ global_variable,
+ model_variables,
+ multiplier_variable,
+ default_value,
+ sensitivity_constraints,
+ existing_constraints=None,
+ sensitivity_type=SensitivityType.equality,
+ remove_bounds=False,
+ ):
+ """Constructor for SensitivityData that stores all model variables,
+ global variable, multiplier variable, default values, and constraints.
+
+ Useing provided information this will be used to build a constraint where
+
+ model_variable = multiplier_variable * global_variable for each model variable
+
+ global_variable value will be used as the baseline for the model variables.
+
+ If sense is equality, model variables will be fixed to the global variable value
+ if sense is upper_bound, model variables will have an upper bound of the global variable value
+ if sense is lower_bound, model variables will have a lower bound of the global variable value
+
+ Args:
+ global_variable: pyomo Var, the global variable to be manipulated
+ model_variables: list of pyomo Vars, the model variables to be manipulated
+ multiplier_variable: pyomo Var, the multiplier variable
+ default_value: float, the default value for the variable
+ constraints: pyomo Constraint, the constraints to be activated/deactivated
+ existing_constraints: pyomo Constraint or list of Constraints, existing constraints to be deactivated/reactivated
+ sensitivity_type: SensitivityType, the type of sensitivity
+ remove_bounds: bool, whether to remove bounds on model variables when activating sensitivity
+ """
+ self.global_variable = global_variable
+ self.model_variables = model_variables
+ self.multiplier_variable = multiplier_variable
+ self.default_value = default_value
+ self.sensitivity_constraints = sensitivity_constraints
+ self.existing_constraints = existing_constraints
+ self.sensitivity_type = sensitivity_type
+ self.remove_bounds = remove_bounds
+
+ def set_default_value(self):
+ """Set the default value for the global variable and fix model variables"""
+
+ self.global_variable.fix(self.default_value.value)
+ for var in self.model_variables:
+ if self.sensitivity_type == SensitivityType.equality:
+ var.fix(self.default_value)
+ print(
+ "Setting default value of " + str(self.default_value),
+ self.default_value.value,
+ )
+ if self.sensitivity_type == SensitivityType.upper_bound:
+ var.setub(value(self.default_value.ub))
+ print(
+ "Setting default upper bound to " + str(self.default_value),
+ self.default_value.ub,
+ )
+ if self.sensitivity_type == SensitivityType.lower_bound:
+ var.setlb(value(self.default_value.lb))
+ print(
+ "Setting default lower bound to " + str(self.default_value),
+ self.default_value.lb,
+ )
+
+ def deactivate_constraints(self):
+ """Deactivate sensitivity constraint and fix all model variables to default value
+ and reactivate existing constraint if provided"""
+ self.sensitivity_constraints.deactivate()
+ for var in self.model_variables:
+ if self.sensitivity_type == SensitivityType.equality:
+ var.fix()
+ if self.existing_constraints is not None:
+ if isinstance(self.existing_constraints, list):
+ for c in self.existing_constraints:
+ print("Reactivating existing constraint", c.name)
+ c.activate()
+ else:
+ print(
+ "Reactivating existing constraint", self.existing_constraints.name
+ )
+ self.existing_constraints.activate()
+
+ def activate_constraints(self):
+ """Activate sensitivity constraint and unfix all model variables
+ and deactivate existing constraints"""
+ self.sensitivity_constraints.activate()
+ for var in self.model_variables:
+ var.unfix()
+ print(
+ "Unfixed variable " + var.name,
+ var.value,
+ "global var",
+ self.global_variable.value,
+ )
+ if self.remove_bounds:
+ var.setlb(None)
+ var.setub(None)
+ print("Removed bounds on variable " + var.name)
+ if self.existing_constraints is not None:
+ if isinstance(self.existing_constraints, list):
+ for c in self.existing_constraints:
+ print("Deactivating existing constraint", c.name)
+ c.deactivate()
+ else:
+ print(
+ "Deactivating existing constraint", self.existing_constraints.name
+ )
+ self.existing_constraints.deactivate()
+
+ def scale_vars_and_constraints(self):
+ """scale constraints and model variables"""
+ iscale.set_scaling_factor(self.multiplier_variable, 1)
+ sf = iscale.get_scaling_factor(self.default_value)
+ if sf is not None:
+ iscale.set_scaling_factor(self.global_variable, sf)
+ for i in self.sensitivity_constraints:
+ print(
+ "Scaling constraint "
+ + self.sensitivity_constraints[i].name
+ + " by "
+ + str(sf)
+ )
+ iscale.constraint_scaling_transform(self.sensitivity_constraints[i], sf)
+
+
+@declare_process_block_class("SensitivityManager")
+class SensitivityManagerData(ProcessBlockData):
+ """Sensitivity manager block for tracking sensitivity vars and creating global
+ sensitivity constraints to manipulate variables on a flowsheets,
+
+ This is designed to aid in performing sensetivity analysis and SVOI analysis by
+ creating global sensitivity variables and constraints.
+
+ Usage guide:
+ Create Sensitivity block on model. m.fs.sensitivity = SensitivityManager()
+
+ Register sensitivities using register_sensitivity method.
+
+ m.fs.sensitivity.register_sensitivity(
+ sensitivity_name="membrane_cost",
+ model_variables=[m.fs.RO_unit[0].membrane_cost, m.fs.RO_unit[1].membrane_cost],
+ default_value=m.fs.RO_unit[0].membrane_cost, # this will use the value from this pyomo var as default
+ sense=SensitivityType.equality, # sets an equality constraint
+ )
+
+ The above will be used to create constraint of where each membrane cost is set to global membrance cost
+ multiplied by a multiplier variable.
+ This will build a membrane_cost variable on m.fs block and a membrane_cost_multiplier variable
+
+ It will then create a constraint for each model variable in provided list (if a list is provided), other wise a single constraint
+ of form
+ m.fs.RO_unit[0].membrane_cost == m.fs.sensitivity.membrane_cost_multiplier * m.fs.sensitivity.membrane_cost
+
+
+ m.fs.sensitivity.register_sensitivity(
+ sensitivity_name="pump_pressure",
+ model_variables=[m.fs.pump[0].outlet.pressure[0], m.fs.pump[1].outlet.pressure[0]],
+ default_value=m.fs.pump[0].outlet.pressure[0], # this will use the value from this pyomo var as default
+ existing_constraints=m.fs.global_pressure_constraint, # this will deactivate this constraint when sensitivity is activated
+ sense=SensitivityType.upper_bound, # sets an upper bound constraint
+ )
+
+ The above will be used to create constraint where each pressure is set to be less then a global variable
+ multiplied by a multiplier variable.
+ This will build a pump_pressure variable on m.fs block and a pump_pressure_multiplier variable
+
+ It will then create a constraint for each model variable in provided list (if a list is provided), other wise a single constraint
+ of form
+ m.fs.pump[0].outlet.pressure[0] <= m.fs.sensitivity.pump_pressure_multiplier * m.fs.sensitivity.pump_pressure
+
+ Once the model is built and all sensitivities are registered, call fix_and_scale method to
+ fix all global variables to default values and scale variables and constraints.
+
+ NOTE: The model sensitivity constraints are deactivated by default to avoid
+ interfering with model initialization.
+ Use the activate_sensitivities method to activate all sensitivity constraints.
+
+ The fix and scale function will fix all default global variables and multiplier variables to 1.
+ WARNING: This will not change variable bounds, ensure to update those before changing multiplier or global variables.
+ or enable bound removal by passing remove_bounds=True to register_sensitivity method.
+
+ Before running sensitivity analysis use the activate_sensitivities method to activate all sensitivity constraints
+ and deactivate any registered existing constraints.
+ After sensitivity analysis use the deactivate_sensitivities method to deactivate all sensitivity constraints and reactivate existing constraints
+ """
+
+ CONFIG = ProcessBlockData.CONFIG()
+
+ def build(self):
+ super().build()
+ self.sensitivities = {}
+
+ def register_sensitivity(
+ self,
+ sensitivity_name,
+ model_variables,
+ default_value=None,
+ sensitivity_type=SensitivityType.equality,
+ existing_constraints=None,
+ remove_bounds=False,
+ ):
+ """Register a sensitivity variable and constraint on the SensitivityManager block.
+ Args:
+ sensitivity: str, the name of the sensitivity
+ model_variables: list of pyomo Vars, the model variables to be manipulated
+ default_value: pyomo Var the default value for the variable, if None, first Var will be used for default
+ sense: SensitivityType, the type of sensitivity
+ existing_constraints: pyomo Constraint or list of Constraints, existing constraints to be deactivated/reactivated
+ remove_bounds: bool, whether to remove bounds on model variables when activating sensitivity
+ """
+ if default_value is None:
+ if isinstance(model_variables, list):
+ default_value = model_variables[0]
+ else:
+ default_value = model_variables
+ print(
+ f"Registering sensitivity for {sensitivity_name} with default value {default_value} {model_variables}"
+ )
+ self.add_component(
+ sensitivity_name,
+ Var(initialize=1, units=default_value.get_units()),
+ )
+ self.find_component(sensitivity_name)
+ self.add_component(
+ f"{sensitivity_name}_multiplier",
+ Var(initialize=1, units=pyunits.dimensionless),
+ )
+ self.find_component(f"{sensitivity_name}_multiplier").fix()
+
+ if isinstance(model_variables, list) is False:
+ model_variables = [model_variables]
+
+ def indexed_constraint(m, i):
+ if sensitivity_type == SensitivityType.equality:
+ return model_variables[i] == self.find_component(
+ f"{sensitivity_name}_multiplier"
+ ) * self.find_component(sensitivity_name)
+
+ if sensitivity_type == SensitivityType.upper_bound:
+ return model_variables[i] <= self.find_component(
+ sensitivity_name
+ ) * self.find_component(f"{sensitivity_name}_multiplier")
+ if sensitivity_type == SensitivityType.lower_bound:
+ return model_variables[i] >= self.find_component(
+ f"{sensitivity_name}_multiplier"
+ ) * self.find_component(sensitivity_name)
+
+ self.add_component(
+ f"eq_{sensitivity_name}",
+ Constraint(list(range(len(model_variables))), rule=indexed_constraint),
+ )
+ self.sensitivities[sensitivity_name] = SensitivityData(
+ self.find_component(sensitivity_name),
+ model_variables,
+ self.find_component(f"{sensitivity_name}_multiplier"),
+ default_value,
+ self.find_component(f"eq_{sensitivity_name}"),
+ existing_constraints=existing_constraints,
+ sensitivity_type=sensitivity_type,
+ remove_bounds=remove_bounds,
+ )
+ self.sensitivities[sensitivity_name].deactivate_constraints()
+ print("Registered sensitivity for " + sensitivity_name)
+
+ def fix_and_scale(self):
+ for sense in self.sensitivities.values():
+ sense.set_default_value()
+ sense.scale_vars_and_constraints()
+
+ def activate_sensitivities(self):
+ for sense in self.sensitivities.values():
+ sense.activate_constraints()
+
+ def deactivate_sensitivities(self):
+ for sense in self.sensitivities.values():
+ sense.deactivate_constraints()
+
+ def display_sensitivities(self):
+ print("----------------Showing Sensitivities----------------")
+
+ for sense in self.sensitivities:
+ print(
+ f"{sense}: default value {self.sensitivities[sense].global_variable.value}, multiplier {self.sensitivities[sense].multiplier_variable.name} {self.sensitivities[sense].multiplier_variable.value} "
+ )
+ print("-----------------------------------------------------")
+
+ def generate_multiplier_yaml_template(
+ self,
+ filename="sweep_template_multiplier.yaml",
+ multiplier_lb=0.8,
+ multiplier_ub=1.2,
+ ):
+ sweep_dict = {}
+ for sense in self.sensitivities:
+ sweep_dict[f"{sense}_multiplier"] = {
+ "type": "LinearSample",
+ "param": self.sensitivities[sense].multiplier_variable.name,
+ "lower_limit": multiplier_lb,
+ "upper_limit": multiplier_ub,
+ "num_samples": 3,
+ }
+ with open(filename, "w") as f:
+ yaml.dump(sweep_dict, f, sort_keys=False)
+ print(f"Generated sweep template file: {filename}")
+
+ def generate_absolute_yaml_template(
+ self,
+ filename="sweep_template_absolute.yaml",
+ multiplier_lb=0.8,
+ multiplier_ub=1.2,
+ ):
+ sweep_dict = {}
+ for sense in self.sensitivities:
+ sweep_dict[f"{sense}"] = {
+ "type": "LinearSample",
+ "param": self.sensitivities[sense].global_variable.name,
+ "lower_limit": self.sensitivities[sense].global_variable.value
+ * multiplier_lb,
+ "upper_limit": self.sensitivities[sense].global_variable.value
+ * multiplier_ub,
+ "num_samples": 3,
+ }
+ with open(filename, "w") as f:
+ yaml.dump(sweep_dict, f, sort_keys=False)
+ print(f"Generated sweep template file: {filename}")
+
+ def generate_multiplier_svoi_template(
+ self, filename="sweep_template_multiplier.yaml"
+ ):
+ sweep_dict = {}
+ sweep_dict["diff_param_loop"] = {}
+ for sense in self.sensitivities:
+ sweep_dict["diff_param_loop"][f"{sense}_multiplier"] = {
+ "diff_mode": "percentile",
+ "diff_sample_type": "UniformSample",
+ "param": self.sensitivities[sense].multiplier_variable.name,
+ "relative_lb": -0.01,
+ "relative_ub": -0.01,
+ "nominal_lb": 0.1,
+ "nominal_ub": 0.2,
+ "num_samples": 10,
+ }
+ sweep_dict["diff_param_loop"]["sweep_reference_params"] = {}
+ for sense in self.sensitivities:
+ sweep_dict["diff_param_loop"]["sweep_reference_params"][
+ f"{sense}_multiplier"
+ ] = {
+ "type": "UniformSample",
+ "param": self.sensitivities[sense].multiplier_variable.name,
+ "lower_limit": 1,
+ "upper_limit": 1,
+ }
+ with open(filename, "w") as f:
+ yaml.dump(sweep_dict, f, sort_keys=False)
+ print(f"Generated sweep template file: {filename}")
+
+ def generate_absolute_svoi_template(
+ self, filename="svoi_sweep_template_absolute.yaml"
+ ):
+ sweep_dict = {}
+ sweep_dict["diff_param_loop"] = {}
+ for sense in self.sensitivities:
+ sweep_dict["diff_param_loop"][f"{sense}"] = {
+ "diff_mode": "percentile",
+ "diff_sample_type": "UniformSample",
+ "param": self.sensitivities[sense].global_variable.name,
+ "relative_lb": -0.01,
+ "relative_ub": -0.01,
+ "nominal_lb": self.sensitivities[sense].global_variable.value,
+ "nominal_ub": self.sensitivities[sense].global_variable.value,
+ "num_samples": 1,
+ }
+ sweep_dict["diff_param_loop"]["sweep_reference_params"] = {}
+ for sense in self.sensitivities:
+ sweep_dict["diff_param_loop"]["sweep_reference_params"][f"{sense}"] = {
+ "type": "UniformSample",
+ "param": self.sensitivities[sense].global_variable.name,
+ "lower_limit": self.sensitivities[sense].global_variable.value,
+ "upper_limit": self.sensitivities[sense].global_variable.value,
+ }
+ with open(filename, "w") as f:
+ yaml.dump(sweep_dict, f, sort_keys=False)
+ print(f"Generated sweep template file: {filename}")