diff --git a/.github/workflows/mpi4py-test.yml b/.github/workflows/mpi4py-test.yml index cad372cc3c..8ad8fdea16 100644 --- a/.github/workflows/mpi4py-test.yml +++ b/.github/workflows/mpi4py-test.yml @@ -64,7 +64,7 @@ jobs: run: conda info - name: Test parallel pytest w/ MPI run: | - mpiexec -n 2 coverage run --parallel-mode -m mpi4py -m pytest watertap/tools/parameter_sweep/tests/test*parameter_sweep.py --no-cov + mpiexec -n 2 coverage run --parallel-mode -m mpi4py -m pytest watertap/tools/parameter_sweep/tests/test*parameter_sweep.py watertap/tools/analysis_tools/loop_tool/tests/test*loop_tool.py --no-cov # single report coverage combine # convert to XML diff --git a/docs/how_to_guides/how_to_use_loopTool_to_explore_flowsheets.rst b/docs/how_to_guides/how_to_use_loopTool_to_explore_flowsheets.rst new file mode 100644 index 0000000000..b9ad2da1f0 --- /dev/null +++ b/docs/how_to_guides/how_to_use_loopTool_to_explore_flowsheets.rst @@ -0,0 +1,502 @@ +How to use loopTool to explore flowsheets +========================================= + +.. index:: + pair: watertap.tools.analysis_tools.loop_tool;loop_tool + +.. currentmodule:: watertap.tools.analysis_tools.loop_tool + +The loopTool is a wrapper for the parameter sweep (PS) tool set, and is designed to simplify setting up parametric sweeps, enabling sweeping over discrete design choices, and providing structured data management. + +The loopTool uses the full features of PS tool set, including standard PS tool and differential PS tool, but brings in the ability to run iteratively over different build options, initialization options, and solve options that might be required for full flowsheet analysis. + +A common example is solving processes with multiple stages such as LSRRO flow sheet, or options where different design choices need to be evaluated such as different pressure exchanger types in RO. Other examples could be exploring different initialization guesses, or setting up different solve constraints. All of these options can be run in nested configuration (e.g. for every stage simulate N number of build configurations). + +The loopTool uses .h5 format for structured data management, and a companion tool dataImporter, provides a simple interface to explore these files (coming soon). The .h5 format enables saving data in a file using directories, such that each unique simulation run with PS tool is stored in its own directory, enabling one file to store a large number of different simulations. The loopTool does not support storing data in CSV format. + +Setting up loopTool +----------------------------- +The loopTool setup involves creating the following: + +1) A flowsheet with build, initialize (optional), and optimize function +2) A .yaml file with simulation configuration +3) A .py file that connects loopTool to the flowsheet, directories for where to save data, and yaml file, as well as other solve or PS options + +Setting up a flowsheet for use with loopTool +---------------------------------------------------- +The loopTool requires a similar functional setup as PS tool kit. Here we will setup RO_with_energy_recovery.py example flowsheet for use with PS and loopTool. The RO_with_energy_recovery flowsheet has an option +that selects the type of ERD used, by passing erd_type option at build (either a pressure_exchanger, pump_as_turbine, or None). The user likely would want to run a parameter sweep across these options, and we will setup the loopTool to do so. + +*The user will need to set up the following three functions:* + +1. The *build_function* that builds the flowsheet (ro_build)- This function should build the flowsheet, and accept any kwargs for its configuration. Herein we pass explicitly erd_type, but also include *kwargs* in case we want to pass in other options in the future. When loopTool runs, it will pass selected *kwargs* into this function before running the sweep. (This function ideally should not initialize the model but should build all relevant vars, parameters, and constraints). + +2. The *initialization_function* (ro_init)- This function should initialize the model and set it up for optimization or simulation run. + + If the user enables update_sweep_params_before_init option (default: False), the PS tool (and loopTool) will update the parameters on the model tree that are being swept across before calling the initialize function, as well as before solving the optimize function. In the current example, we will use the default setting. + + *Your initialization function should also be used to prepare the model for solving or optimization run, and thus should call on any additional functions to do so. In our example, a function “optimize_set_up” needs to be called before we run actual optimization. This function unfixes variables we want to optimize and fixes those that should be fixed. For PS/LoopTool, it does not matter if you are planning to do optimization (>0 DOFS) or simulation (0 DOFs), and as such you can set up the model to do either.* + +3. The *optimize_function* this function will run your optimization or simulation, and should return the result. In general, you are free to include tests to see if the result is optimal, but PS/loopTool will do that check before saving data from the solution. A common error is to have an optimization function that does not return a result, resulting in all of the solves failing during run, even if the model is successfully solved. + + *The optimize function can include complex solution steps beyond just running the solve call. For example, a user can specify in the optimize function to first solve a model using a continuous approach, followed by a resolve with discrete choices. The user just needs to remember that PS tool will update the sweep parameters only before the call, but not verify that they were applied after execution.* + +An example of the functions setup for use with loopTool for RO_with_energy_recovery flowsheet example. + +.. code-block:: + + import watertap.examples.flowsheets.RO_with_energy_recovery.RO_with_energy_recovery as ro_erd + def ro_build(erd_type=None, **kwargs): + m = ro_erd.build(erd_type) + return m + + def ro_init(m, solver=None, **kwargs): + ro_erd.set_operating_conditions( + m, water_recovery=0.5, over_pressure=0.3, solver=solver + ) + ro_erd.initialize_system(m, solver=solver) + ro_erd.optimize_set_up(m) + + def ro_solve(m, solver=None, **kwargs): + result = solver.solve(m) + return result + +Setting up the .yaml configuration file +-------------------------------------------------- +The loopTool uses a .yaml configuration file to generate all sweep configurations, define variables to sweep over, and other options. +This enables user to have multiple setup files that can be used to run simulations without having to change any underlying code, except changing which .yaml file the loopTool uses. + +**The loopTool .yaml configuration file accepts two types of options:** + + * *default options* - these configure simulation defaults that either define PS tool behavior, loopTool behavior, or default keys that are passed into build, initialize, or optimize functions unless they are overridden by loop options + * *loop options* - these define options that build iterative loops, and will override existing default values or be included with them. + +**default options:** + + * *initialize_before_sweep* –(default: False) option to force run initialization_function before every optimize_function call + * *update_sweep_params_before_init* –(default: False) if set to true, will result in parameters being swept over to be updated before the initialize call. + * *number_of_subprocesses* –(default: 1) Number of logical cores to use if running in a parallel manner (not used with MPI or RayIo in cluster mode), default: 1 + * *parallel_back_end* – (default: MultiProcessing) Parallel back end to use if number_of_subprocesses > 1. (refer to Parallel manager doc for further info) + * *build_defaults* – default arguments that are passed for every sweep. For example in ro_erd flowsheet, shown here, this could be erd_type, but in other flowsheets, this could define any default options, from file locations to the number of stages, etc. The defaults will be passed along with loop values unless the loop value overrides them. + * *init_defaults* - defaults for initialization_function, same behavior as build-defaults but used for initialization_function + * *optimize_defaults* - defaults for optimize_function, same behavior as build or init defaults, but for optimize function. + * *build_output_kwargs * - a list of keys to only save in .h5 file. loopTool provides a default build_outputs function that takes in a dict containing model key name and model key (e.g., to output only m.fs.costing.LCOW user would include following dict *LCOW : fs.costing.LCOW*). The default function uses model.find_component to construct an output dict. Alternatively user can provide kwargs to a user-provided build_outputs function that would be linked to loopTool (shown below). + +**loop options:** + + * *build_loop* - defines list of keywords arguments to loop over for the *build_function* + * *init_loop* - defines list of keywords arguments to loop over for the *initialization_function* + * *optimize_loop* - defines list of keywords arguments to loop over for the *optimize_function* + * *sim_cases* - defines specific cases to run for given build_loop, init_loop, or optimize_loop. + * *sweep_param_loop* - defines sweep parameters to loop over + * *diff_param_loop* - defines differential parameter sweep, the first set of sweep_params define differential sweep parameters, and second key has to be *sweep_reference_params* - which defines the parameters to use for generation of references simulations, from which differential simulations are performed. + +**Defining sweep_param_loop and diff_param_loop:** + +The sweep_param_loop and diff_param_loop define the parameters to perform sweeps over using PS tool kit, and support standard configurations in the following structure. + +These configuration arguments are designed to be defined and set up in a .yaml file. The general structure for .yaml file structure should be as follows: + +.. code-block:: + + # for parametric sweeps + # for LinearSamples, UniformSample, GeomSample, ReverseGeomSample, LatinHypercubeSample + sweep_param_loop: + sweep_param_name: + type: Any type supported by PS toolkit (LinearSample, UniformSample, etc.) + param: key on the flowsheet that can be found using m.find_component (e.g., fs.costing.reverse_osmosis.membrane_cost) + lower_limit: lower value for sampling + upper_limit: upper value for sampling + num_samples: number of samples to run + + # for NormalSample + sweep_param_loop: + sweep_param_name: + type: Any type supported by PS toolkit (NormalSample only) + param: key on the flowsheet that can be found using m.find_component (e.g., fs.costing.reverse_osmosis.membrane_cost) + mean: mean value of normal sample + std: standard deviation + num_samples: number of samples to run + +The sweep_param_loop parameters will be iterated over one by one, to sweep over multiple parameters at once you can define a group as follows: + +.. code-block:: + + # for parametric sweeps + # for LinearSamples, UniformSample, GeomSample, ReverseGeomSample, LatinHypercubeSample + sweep_param_loop: + sweep_group_name: + sweep_param_name_1: + type: Any type supported by PS toolkit (LinearSample, UniformSample, etc.) + param: key on the flowsheet that can be found using m.find_component (e.g., fs.costing.reverse_osmosis.membrane_cost) + lower_limit: lower value for sampling + upper_limit: upper value for sampling + num_samples: number of samples to run + sweep_param_name_2: + type: Any type supported by PS toolkit (LinearSample, UniformSample, etc.) + param: key on the flowsheet that can be found using m.find_component (e.g., fs.costing.reverse_osmosis.membrane_cost) + lower_limit: lower value for sampling + upper_limit: upper value for sampling + num_samples: number of samples to run + sweep_param_name_N: + type: Any type supported by PS toolkit (LinearSample, UniformSample, etc.) + param: key on the flowsheet that can be found using m.find_component (e.g., fs.costing.reverse_osmosis.membrane_cost) + lower_limit: lower value for sampling + upper_limit: upper value for sampling + num_samples: number of samples to run + +**Defining diff_param_loop** + +The diff_param_loop defines a run for parameter_sweep_differential tool and requires the same parametrization. Namely the differential spec and parameter sweep values. In the .yaml file, when setting up diff_param_loop, the first set of parameters will define the differential spec, while values after *sweep_reference_params* will define the parameters for reference sweep. + +.. code-block:: + + # for differenatial sweeps + diff_param_loop: + # percentile diff_type + diff_param_name: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: lower nominal value + nominal_ub: upper nominal value + num_samples: number of samples, if you have no difference between relative_lb and relative_ub, set to 1 + param: key on the flowsheet that can be found using m.find_component (e.g., fs.costing.reverse_osmosis.membrane_cost) + relative_lb: lower percentile bound to sample + relative_ub: upper percentile bound to sample, can be same as relative lb, to sample single value + # sum or product diff_type + diff_param_name: + diff_mode: sum or product + diff_sample_type: UniformSample + num_samples: number of samples, if you have no difference between relative_lb and relative_ub, set to 1 + param: key on the flowsheet that can be found using m.find_component (e.g., fs.costing.reverse_osmosis.membrane_cost) + relative_lb: lower relative value to sample + relative_ub: upper relative value to sample + sweep_reference_params: # normal sweep-param input to generate reference simulations + sweep_param_name: + type: Any type supported by PS toolkit (LinearSample, UniformSample, etc.) + param: key on the flowsheet that can be found using m.find_component (e.g., fs.costing.reverse_osmosis.membrane_cost) + mean: mean value of normal sample + sd: standard deviation + num_samples: number of samples to run + +**General .yaml configuration file structure:** + +The general structure starts with the analysis name, which will be used in the file name when saving, followed by default options and configurations, and finally by loops options. The general structure is shown below: + +.. code-block:: + + analysis_name: + initialize_before_sweep: (optional) False or True + update_sweep_params_before_init: (optional) False or True + number_of_subprocesses: (optional) int > 1 + parallel_back_end: (optional) MultiProcessing, RayIo, etc. + build_defaults: (optional) + default_a: value_a + init_defaults: (optional) + init_a: value_a + optimize_defaults: (optional) + opt_a: value_a + build_loop: + buld_arg: + - value_a + - vlaue_b + - etc + init_loop: + init_arg: + - value_a + - vlaue_b + - etc + etc_loop: + etc_arg: + - etc_val + param_sweep_loop: + sweep_param_name_a: + sweep_param_configs + sweep_param_name_b: + sweep_param_configs + sweep_parm_etc: + etc + +**Examples for RO with ERD** + +Here we setup a simple run on RO erd flowsheet, requesting loopTool to run PS tool over two RO erd_type configurations, for each erd_configuration, we run a linear sweep over membrane cost, a linear sweep over factor_membrane_replacment, and map sweep over NaCl loading and RO recovery, the map sweep will generate a mesh grid using NaCl loading and RO recovery, producing 9 samples total. The example of map sweep can include as many or as few parameters as user desires. + +.. code-block:: + + ro_erd_type_analysis: + reinitialize_before_sweep: False # We don't need to reinit before each solve + build_output_kwargs: + LCOW : fs.costing.LCOW # only save process cost in h5file + build_loop: # We are only gonna loop over each build function + erd_type: + - pressure_exchanger + - pump_as_turbine + sweep_param_loop + membrane_cost: # Runs over differnt membrnae costs + type: LinearSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 10 + upper_limit: 30 + num_samples: 3 + factor_membrane_replacement: # Runs over membrane_replacment costs, generating 10 steps + type: LinearSample + param: fs.costing.reverse_osmosis.factor_membrane_replacement + lower_limit: 0.1 + upper_limit: 0.2 + num_samples: 3 + map_sweep: # Will run meshgrid sweep over feed_mass_nacl and ro_recovery, generating 100 samples + feed_mass_nacl: # Runs over salt mass flow rate only, generating 10 steps + type: LinearSample + param: fs.feed.properties[0].flow_mass_phase_comp[Liq,NaCl] + lower_limit: 0.03 + upper_limit: 0.04 + num_samples: 3 + ro_recovery: # Runs over ro recovery, generating 10 steps + type: LinearSample + param: fs.RO.recovery_mass_phase_comp[0,Liq,H2O] + lower_limit: 0.3 + upper_limit: 0.5 + num_samples: 3 + +Example using sim_case, here we assume ro_build function takes in 2 options, a water type, and erd type. +The loop tool will ran a parameter sweep over each case. + +.. code-block:: + + ro_erd_analysis_simple: + build_loop: + sim_cases: + BGW_with_erd: + water_type: BGW + erd_type: pump_as_turbine + BGW_without_ERD: + water_type: BGW + erd_type: null # none in .yaml is null + SW_with_ERD: + water_type: SW + erd_type: pump_as_turbine + sweep_param_loop: + membrane_cost: # Runs over different membrane costs + type: LinearSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 10 + upper_limit: 30 + num_samples: 3 + +Example when we don't do any build loops, init loops etc., this will simply run membrane_cost sweep. This is the same as using PS tool directly, but it gives a simple data management structure if user wants to sweep over many variables in the model. + +.. code-block:: + + ro_erd_analysis_simple: + sweep_param_loop: + membrane_cost: # Runs over different membrane costs + type: LinearSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 10 + upper_limit: 30 + num_samples: 3 + +Example of setting up differential sweep. + +*Briefly, this type of analysis can provide insight into how reducing membrane cost and increasing membrane lifespan( reducing replacement rate), can reduce RO costs, even when the exact membrane cost is not known. This sweep will produce reference cost-optimal RO designs with different membrane cost, and for each design, we will simulate a differential design where only the membrane cost, or membrane replacement rate is reduced. The difference between the reference designs and differential design in LCOW provides a quantitative measure in how reducing membrane cost or reducing replacement cost would typically impact cost of RO. In practice, we would vary many design and decision variables for RO as they are uncertain, and this could include replacement factors, membrane performance metrics, pump costs, etc. All of these could be added to sweep_reference_params.* + +*Additionally, diff spec params in diff_param_loop can be provided in param_groups like with the standard parameter sweep shown above, allowing changing multiple parameters at the same time.* + +.. code-block:: + + ro_diff_analysis: + reinitialize_before_sweep: False # We don't need to reinit before each solve + build_loop: # We are only gonna loop over each build function + erd_type: + - pressure_exchanger + - pump_as_turbine + diff_param_loop: #the params below will be run iteratively, differentiating from the simulation created with sweep_reference_params + membrane_cost: # Runs over different membrane costs + diff_mode: percentile + diff_sample_type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + nominal_lb: 10 + nominal_ub: 30 + num_samples: 1 + factor_membrane_replacement: # Runs over different replacement rates + diff_mode: percentile + diff_sample_type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + nominal_lb: 0.1 + nominal_ub: 0.2 + num_samples: 1 + sweep_reference_params: # The parameters below will be used to generate reference design and will be run simultaneously (e.g. all of them will be changed to generate a new hypothetical design here, we only provide one parameter, membrane cost, but could provide many others) + membrane_cost: + type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 10 + upper_limit: 30 + num_samples: 10 + +Setting up the loopTool +------------------------------------- + +To loopTool can be executed by passing in the flowsheet functions, .yaml configuraiton file, and save locations into the loopTool, as well as additional optional arguments: + + * loop_file (required): .yaml config file that contains iterative loops to run + * solver (optional): solver to use in model, default uses watertap solver + * build_function (required): function to build unit model + * initialize_function (optional): function for initialization of the unit model + * optimize_function (required): function for solving model + * build_outputs : (optional) function to build selected outputs, if not provided, defaults to a loopTool built-in default function when user provides build_outputs_kwargs in .yaml configuration file. + * probe_function (optional): Function to probe if a solution should be attempted or not + * save_name (required): name to use when saving the file with + * save_dir (required): directory to save the file in + * number_of_subprocesses (optional): user defined number of subprocesses to use for parallel run should be 1 or greater + * parallel_back_end (optional): backend to use for a parallel run if MPI is not used and number_of_subprocesses > 1 + * custom_do_param_sweep (optional): custom param function (refer to parameter sweep tool) + * custom_do_param_sweep_kwargs (optional): custom parm kwargs (refer to parameter sweep tool) + * execute_simulations (optional): sets if looptool should execute simulations upon setup, otherwise user can call build_run_dict, and run_simulations call manually + * h5_backup (optional): Set location for backup file, if set to False, no backup will be created, otherwise the backup will be auto-created + +Example of code for setting up our example of RO with ERD + +.. code-block:: + + # This imports the function created in the example for RO_with_energy_recovery above + import ro_erd as ro_setup + # import the loopTool and utility function for getting a working directory + from watertap.tools.analysis_tools.loop_tool.loop_tool import loopTool, get_working_dir + + if __name__=='__main__': we will execute the loopTool script here, required for safe execution of parallel scripts + # We assume our .yaml file is in the same directory as this script + + lp = loopTool( + "ro_erd_sweep.yaml", # name of the yaml files we created in the example above + build_function=ro_setup.ro_build, # our build_function + initialize_function=ro_setup.ro_init, # our initialize function + optimize_function=ro_setup.ro_solve, # our solve function + saving_dir=get_working_dir(), # this gets working directory for script so we can save files in same dirctory + save_name="ro_with_erd", # this will be the name of this loop run + parallel_back_end=”RayIo”, # backend we want to use for parallel runs + number_of_subprocesses = 8, #use 8 logical cores on our computer + ) + +The above code example will run the RO_erd flowsheet through loops specified with our .yaml file using RayIo for parallel backend and 8 processes.. + +The loopTool will create an output folder in the directory as found by get_working_dir(). + +Upon succesfull run there will be an output folder with an h5 File that has name with the following structure **save_name_analysisType_analysis_name**. The save_name is specified in loopTool, while analysis_name is specified in .yaml configuration file (this means you can have multiple analysis types in the same .yaml configuration file, and each analysis will be saved in its own file). + +Output data structure and data protection +----------------------------------------- + +The loopTool will store data in h5 file, with a structure similar to that of the .yaml file being run. For example, for the ro_erd_type_analysis example, the h5 file structure would be as follows: + +.. code-block:: + + ro_erd_type_analysis + |-erd_type + |-pressure_exchanger + | |-membrane_cost # contains standard PS h5 output + | | |-outputs + | | |-solve_successful + | | |-sweep_params + | | + | |-factor_membrane_replacement # contains standard PS h5 output + | | |-outputs + | | |-solve_successful + | | |-sweep_params + | | + | |-map_sweep # contains standard PS h5 output + | |-outputs + | |-solve_successful + | |-sweep_params + | + |-pump_as_turbine + |-membrane_cost # contains standard PS h5 output + | |-outputs + | |-solve_successful + | |-sweep_params + | + |-factor_membrane_replacement # contains standard PS h5 output + | |-outputs + | |-solve_successful + | |-sweep_params + | + |-map_sweep # contains standard PS h5 output + |-outputs + |-solve_successful + |-sweep_params + +We can readily access the data for processing by using h5py. +To visually explore the h5 file, use HDFviewer ( https://www.hdfgroup.org/downloads/hdfview/ ) +All the parameters will use their parm names or object keys as reference (e.g. if you set up yaml file to sweep over 'RO_recovery' for which param is 'fs.ro.ro_recovery', the file will store data for RO_recovery under key 'fs.ro.ro_recovery'. Alternatively, if you provided build_outputs_kwargs, and specified a name for a key (e.g., *RO recovery: fs.ro.ro_recovery* than this result will be saved under *RO recovery*. + +.. code-block:: + + import h5py + h5file = h5py.File( + "ro_with_erd_analysisType_ro_erd_type_analysis.h5", "r" + ) + + # get data for cost from pressure_exchanger erd device, not the dir path structure + # if not providing any outputs + data = h5file[ + "ro_erd_type_analysis/erd_type/pressure_exchanger/membrane_cost/outputs/fs.ro.ro_recovery/value" + ][()] # wil create a list + + # if specified output names with build_output_kwargs (RO recovery) + data = h5file[ + "ro_erd_type_analysis/erd_type/pressure_exchanger/membrane_cost/outputs/RO recovery/value" + ][()] # wil create a list + +**Backup management** + +The loopTool includes a naïve data management schema to prevent overwriting existing files and minimize simulation runs. This is accomplished by: + +**Creating backups:** The loopTool will never overwrite exiting file. When the loopTool starts it will check if a file with the same name and directory already exists. If it does, it will rename that file to include a date and time (file_name_M_D-H_M-S.h5.bak). + + +**Checking existng solutions:** The backup file will be used to check if existing completed simulations exist before running a simulation. It will check if the backup file contains a complete simulation for the current run, (this only checks the number of successfully solved samples but does not check if the sweep_parameters match). If all of the simulations were successfully solved in the backup file, it would copy over the data from the backup file into the new file. Otherwise, it will re-run the simulations. +The user can specify the expected number of samples if it differs from num_samples by passing additional *expected_num_samples*, or if there is minimum number of samples expected using *min_num_samples*. +User can also specify to force a re-run or to not re-run by passing *force_rerun*. +Example of use as follows: + +.. code-block:: + + # single parameter + ro_erd_analysis_simple: + sweep_param_loop: + membrane_cost: # Runs over different membrane costs + type: LinearSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 10 + upper_limit: 30 + num_samples: 3 + min_num_samples: int (if set, loopTool will only re-run if there is fewer samples in the backup file than the set value) + expected_num_samples: int (if set, loopTool will only re-run if the expected_num_samples differs from the number of samples saved in the backup file) + force_rerun: False or True (if set to True, will force to always rerun given parameter sweep, if False, will not re-run the parameter sweep) + map_sweep: # Will run meshgrid sweep over feed_mass_nacl and ro_recovery, generating 100 samples + feed_mass_nacl: # Runs over salt mass flow rate only, generating 10 steps + type: LinearSample + param: fs.feed.properties[0].flow_mass_phase_comp[Liq,NaCl] + lower_limit: 0.03 + upper_limit: 0.04 + num_samples: 3 + ro_recovery: # Runs over ro recovery, generating 10 steps + type: LinearSample + param: fs.RO.recovery_mass_phase_comp[0,Liq,H2O] + lower_limit: 0.3 + upper_limit: 0.5 + num_samples: 3 + min_num_samples: int (if set, loopTool will only re-run if there is less samples in the backup file than the set value) + expected_num_samples: int (if set, loopTool will only re-run if the expected_num_samples differs from the number of samples saved in the backup file) + force_rerun: False or True (if set to True, will force to always rerun given parameter sweep, if False, will not re-run the parameter sweep) + +This feature is designed to help with getting complete simulation sets, without needing to re-run all the looped options. For example, you might run a certain build loop, where only in one build option, some solutions failed. After fixing the reason, you can rerun the loopTool, and it will only rerun those build options that failed to solve. + +The user can disable the backup generation by setting h5_backup argument in loopTool to False, or, ideally, by deleting the existing h5 file if the simulation results in it are not needed. The loopTool will avoid overwriting existing files, and if a file with the same name exists, it will error out. + + + + diff --git a/docs/how_to_guides/index.rst b/docs/how_to_guides/index.rst index 170685defd..7c14601eca 100644 --- a/docs/how_to_guides/index.rst +++ b/docs/how_to_guides/index.rst @@ -19,6 +19,7 @@ How To Guides how_to_use_parameter_sweep how_to_use_parameter_sweep_monte_carlo how_to_run_differential_parameter_sweep + how_to_use_loopTool_to_explore_flowsheets how_to_install_electrolyte_database how_to_use_EDB how_to_use_ui_api diff --git a/docs/technical_reference/tools/index.rst b/docs/technical_reference/tools/index.rst index 74835fe848..ffbcfc7131 100644 --- a/docs/technical_reference/tools/index.rst +++ b/docs/technical_reference/tools/index.rst @@ -5,3 +5,4 @@ Tools for Flowsheet Analysis :maxdepth: 2 parameter_sweep + \ No newline at end of file diff --git a/watertap/tools/analysis_tools/loop_tool/data_merging_tool.py b/watertap/tools/analysis_tools/loop_tool/data_merging_tool.py new file mode 100644 index 0000000000..4a01a89211 --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/data_merging_tool.py @@ -0,0 +1,128 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + + +import h5py +import numpy as np +import time +import os +import datetime +from datetime import datetime + +import logging + +__author__ = "Alexander V. Dudchenko (SLAC)" + + +_log = logging.getLogger(__name__) + + +def merge_data_into_file( + file_name, + backup_file_name, + directory, + expected_solved_values=None, + min_solve_values=None, + force_rerun=None, +): + """ + This function checks if there is a sim file, and a back up file. + if there is a sim file, it backs it up, and checks if full solution set already exsts and copies it over, + other wise it creates a new group in actual file, and adds new solved data set to it + """ + run_sweep = True + if os.path.isfile(file_name) == False: + create_h5_file(file_name) + h5file = h5py.File(file_name, "a") + + # check if there is a back up + if isinstance(backup_file_name, str): + f_old_solutions = h5py.File(backup_file_name, "r") + solved_values = sum( + np.array( + f_old_solutions[directory]["solve_successful"]["solve_successful"][()] + ) + ) + else: + solved_values = None + if force_rerun: + _log.info("Forcing a rerun") + run_sweep = False + elif force_rerun == False: + _log.info("Forced to not rerun") + run_sweep = False + elif force_rerun == None and solved_values is not None: + if min_solve_values is not None: + if min_solve_values <= solved_values: + run_sweep = False + elif expected_solved_values == solved_values: + run_sweep = False + _log.info( + "Found {} solved values, expected {} solved values , min {} solved values, re-running == {}".format( + solved_values, expected_solved_values, min_solve_values, run_sweep + ) + ) + if run_sweep: + if directory not in h5file: + h5file.create_group(directory) + elif backup_file_name is not None and os.path.isfile(backup_file_name): + if directory not in h5file: + h5file.copy(f_old_solutions[directory], directory) + else: + _log.warning( + "Solution already {} exist in file, not copying over back up data".format( + directory + ) + ) + f_old_solutions.close() + h5file.close() + return run_sweep + + +def create_backup_file(file_name, backup_name, h5_dir): + """used to created file and back up file + file_name - orignal h5 file + backup_name - backup name for h5 file if exists + h5_dir - directory to check in h5 file, if not there creates fresh file, otherwise + renames existing file""" + + if backup_name is None and os.path.isfile(file_name): + h5file = h5py.File(file_name, "r") + + if h5_dir in h5file: + # need to close file before renaming it + h5file.close() + date = datetime.now().strftime("%d_%m-%H_%M_%S") + backup_name = file_name + "_{}_{}".format(date, ".bak") + if os.path.isfile(backup_name) == False: + os.rename(file_name, backup_name) + else: + # close it in case we did not rename + h5file.close() + return backup_name + + +def create_h5_file(file_name): + """used to created h5file, retry in case disk is busy""" + if os.path.isfile(file_name) == False: + file_created = False + for i in range(60): + try: + f = h5py.File(file_name, "w") + f.close() + file_created = True + break + except: + _log.warning("Could note create h5 file {}".format(file_name)) + time.sleep(0.01) # Waiting to see if file is free to create again + if file_created == False: + raise OSError("Could not create file {}".format(file_name)) diff --git a/watertap/tools/analysis_tools/loop_tool/loop_tool.py b/watertap/tools/analysis_tools/loop_tool/loop_tool.py new file mode 100644 index 0000000000..aa6dbd6f9b --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/loop_tool.py @@ -0,0 +1,725 @@ +############################################################################### +# WaterTAP Copyright (c) 2021, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National +# Laboratory, National Renewable Energy Laboratory, and National Energy +# Technology Laboratory (subject to receipt of any required approvals from +# the U.S. Dept. of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +# +############################################################################### + +from idaes.core.solvers import get_solver + +from watertap.tools.parameter_sweep import ( + ParameterSweep, + RecursiveParameterSweep, +) + +from watertap.tools.parameter_sweep import ( + DifferentialParameterSweep, +) + +from watertap.tools.parameter_sweep import ParameterSweepReader + +from watertap.tools.parameter_sweep.parameter_sweep_differential import ( + DifferentialParameterSweep, +) + +from watertap.tools.analysis_tools.loop_tool.data_merging_tool import * + +from watertap.tools.parallel.parallel_manager_factory import ( + has_mpi_peer_processes, + get_mpi_comm_process, +) +import copy +import os +import h5py +import numpy as np + +__author__ = "Alexander V. Dudchenko (SLAC)" + + +def get_working_dir(): + cwd = os.getcwd() + return cwd + + +class loopTool: + def __init__( + self, + loop_file, + solver=None, + build_function=None, + initialize_function=None, + optimize_function=None, + build_outputs=None, + number_of_subprocesses=None, + parallel_back_end="MultiProcessing", + probe_function=None, + save_name=None, + saving_dir=None, + execute_simulations=True, + custom_do_param_sweep=None, + custom_do_param_sweep_kwargs=None, + h5_backup=None, + ): + """ + Loop tool class that runs iterative paramter sweeps + + Arguments: + loop_file : .yaml config file that contains iterative loops to run + solver : solver to use in model, default uses watertap solver + build_function : function to build unit model + initialize_function : function for intilization of th eunit model + optimize_function : function for solving model + build_outputs : function to build selected outputs + probe_function : Function to probe if a solution should be attempted or not + save_name : name to use when saving the file with + save_dir : directory to save the file in + number_of_subprocesses : user defined number of subprocesses to use for parallel run, defaults to either + max number of logical cores, if set to False, will disable MPI and set number_of_subpressess to 0 + custom_do_param_sweep : custom param function (refer to parameter sweep tool) + custom_do_param_sweep_kwargs : custom parm kwargs (refer to parameter sweep tool) + + execute_simulations : of looptool should execute simulations upon setup, + other user can call build_run_dict, and run_simulations call manually + h5_backup : Set location for back up file, if set to False, no backup will be created, otherwise backup will be autocreated + + """ + + self.loop_file = loop_file + self.solver = solver + + self.build_function = build_function + self.initialize_function = initialize_function + self.optimize_function = optimize_function + self.probe_function = probe_function + + self.save_name = save_name + self.data_dir = saving_dir + + self.number_of_subprocesses = number_of_subprocesses + self.parallel_back_end = parallel_back_end + + self.test_mode = False + + self.custom_do_param_sweep = custom_do_param_sweep + self.custom_do_param_sweep_kwargs = custom_do_param_sweep_kwargs + + self.build_outputs = build_outputs + self.h5_backup_location = h5_backup + + """ supported keys for yaml config file. please update as new options are added""" + self._supported_default_options = [ + "initialize_before_sweep", + "update_sweep_params_before_init", + "number_of_subprocesses", + "parallel_back_end", + "build_defaults", + "init_defaults", + "optimize_defaults", + "build_outputs_kwargs", + ] + self._supported_loop_options = [ + "build_loop", + "init_loop", + "optimize_loop", + ] + self._reserved_loop_options = ["sim_cases"] + self._supported_sweep_options = ["sweep_param_loop", "diff_param_loop"] + + if execute_simulations: + self.build_run_dict() + self.run_simulations() + + def build_run_dict(self, test_setups=False): + """ + This builds the dict that will be used for simulatiuons + + Arguments: + test_setups : test if configuraiton will intilaize, but not run the simulatuons + """ + + loop_dict = ParameterSweepReader()._yaml_to_dict(self.loop_file) + self.sweep_directory = {} + for key, loop in loop_dict.items(): + self.check_dict_keys(loop) + self.sweep_directory[key] = {} + self.init_sim_options() + self.save_dir = self.save_dir + "/" + key + self.h5_directory = key + loop_type = self.get_loop_type(loop) + self.options = loop + self.sweep_directory[key], _ = self.build_sweep_directories( + loop[loop_type], + loop_type, + self.sweep_directory[key], + self.save_dir, + self.h5_directory, + ) + if test_setups: + self.execute_sweep(self.sweep_directory[key], False) + + def check_dict_keys(self, test_dict): + """used to test supported key in provided .yaml file""" + for key in test_dict: + if key not in self._supported_default_options: + if ( + key not in self._supported_loop_options + and key not in self._supported_sweep_options + ): + raise KeyError("Unsupported key {}".format(key)) + elif key in self._supported_sweep_options: + test_result = True + else: + test_result = self.check_loop_keys(test_dict[key]) + if test_result == False: + raise KeyError( + "sweep_param_loop or diff_param_loop not found in config file!" + ) + + def check_loop_keys(self, loop_keys): + """tests loop keys""" + test_result = False + for key in loop_keys: + if key in self._reserved_loop_options: + test_result = self.check_loop_keys(loop_keys[key]) + if test_result == True: + raise KeyError( + "Incorrect usage of sim_cases, which is a reserved options for loop option, review docs" + ) + elif key in self._supported_loop_options: + test_result = self.check_loop_keys(loop_keys[key]) + for key in loop_keys: + if key in self._supported_sweep_options: + test_result = True + + return test_result + + def run_simulations(self): + """runs the simulations created in build_run_dict""" + + self.execute_sweep(self.sweep_directory) + + def run_model_test(self): + """function to run a single test, with out param sweep""" + self.test_mode = True + self.execute_sweep(self.sweep_directory) + self.test_mode = False + + def get_loop_type(self, loop): + """containst types of loop options that are extracted from yaml file + build_loop - options that are passed into build function + init_loop - options that are passed into initialization function + optimize_loop - options that are passed into optimize function + sweep_param_loop - this will creates the paramters that should be sweeped over + diff_param_loop - parmaters for differential sweeps + """ + + if "build_loop" in loop: + loop_type = "build_loop" + elif "init_loop" in loop: + loop_type = "init_loop" + elif "optimize_loop" in loop: + loop_type = "optimize_loop" + elif "sweep_param_loop" in loop: + loop_type = "sweep_param_loop" + elif "diff_param_loop" in loop: + loop_type = "diff_param_loop" + else: + loop_type = None + return loop_type + + def get_loop_key(self, loop, loop_type): + """function for finding loop type""" + for key in loop.keys(): + if key != loop_type: + return key + + def build_sweep_directories( + self, loop, loop_type, sweep_directory, cur_dir, cur_h5_dir + ): + """this creats the loop directory dict, which is then used to run + the paramter sweep""" + if loop_type != None: + loop_type_recursive = self.get_loop_type(loop) + loop_key_current = self.get_loop_key(loop, loop_type) + if loop_type_recursive != None: + sweep_directory[loop_key_current] = {} + for loop_value in loop[loop_key_current]: + if isinstance(loop_value, dict): + loop_value = str(loop_value).strip("{}") + sweep_directory[loop_key_current][loop_value] = {} + cur_dir = self.update_dir_path( + cur_dir, loop_key_current, loop_value + ) + cur_h5_dir = self.update_dir_path( + cur_h5_dir, loop_key_current, loop_value + ) + self.update_sim_options( + loop_type, loop_key_current, loop_value, loop + ) + ( + sweep_directory[loop_key_current][loop_value], + cur_dir, + ) = self.build_sweep_directories( + loop[loop_type_recursive], + loop_type_recursive, + sweep_directory[loop_key_current][loop_value], + cur_dir, + cur_h5_dir, + ) + else: + for loop_value in loop: + local_dir = self.update_dir_path(cur_dir, loop_value, value=None) + h5_local_dir = self.update_dir_path( + cur_h5_dir, loop_value, value=None + ) + self.update_sim_options(loop_type, loop_value, loop, None) + # creates directory dict with all options + if bool(self.sweep_params): + sweep_directory[loop_value] = { + "simulation_setup": { + "dir": local_dir, + "h5dir": h5_local_dir, + "build_defaults": copy.deepcopy(self.build_defaults), + "init_defaults": copy.deepcopy(self.init_defaults), + "optimize_defaults": copy.deepcopy( + self.optimize_defaults + ), + "sweep_params": copy.deepcopy(self.sweep_params), + "num_samples": copy.deepcopy(self.num_samples), + "expected_num_samples": copy.deepcopy( + self.expected_num_samples + ), + "force_rerun": copy.deepcopy(self.force_rerun), + "min_num_samples": copy.deepcopy(self.min_num_samples), + "diff_params": copy.deepcopy(self.diff_params), + "diff_samples": copy.deepcopy(self.diff_samples), + "original_options_dict": copy.deepcopy(self.options), + } + } + + return sweep_directory, cur_dir + + def update_dir_path(self, cur_dir, key, value): + """creates directory pathing for h5 file strucutre""" + if value == None: + cur_dir = cur_dir + "/" + str(key) + else: + if cur_dir.find(key) > 0: + cur_dir = cur_dir[: (cur_dir.find(key) - 1)] + cur_dir = cur_dir + "/" + str(key) + cur_dir = cur_dir + "/" + str(value) + return cur_dir + + def update_sim_options(self, loop_type, loop_key, loop_value, loop): + """used to update simulation options for each specific loop""" + + if loop_type == "build_loop": + self.build_defaults.update(self.get_loop_params(loop_key, loop_value, loop)) + elif loop_type == "init_loop": + self.init_defaults.update(self.get_loop_params(loop_key, loop_value, loop)) + elif loop_type == "optimize_loop": + self.optimize_defaults.update( + self.get_loop_params(loop_key, loop_value, loop) + ) + elif loop_type == "sweep_param_loop": + self.sweep_params = {} + ( + self.sweep_params, + self.num_samples, + self.expected_num_samples, + self.min_num_samples, + self.force_rerun, + ) = self.get_sweep_params(loop_key, loop_value) + + elif loop_type == "diff_param_loop": + self.sweep_params = {} + self.diff_params = {} + if loop_key != "sweep_reference_params": + ( + self.sweep_params, + self.num_samples, + self.diff_params, + self.diff_samples, + self.expected_num_samples, + self.min_num_samples, + self.force_rerun, + ) = self.get_diff_params(loop_key, loop_value) + + def get_loop_params(self, key, loop_value, loop): + if "sim_cases" in key: + return loop[key][loop_value] + elif ":" in str(loop_value): + lp = loop_value.split(":") + return {key: {eval(lp[0]): float(lp[1])}} + else: + return {key: loop_value} + + def get_diff_params(self, key, loop_value): + """creates dict for differntial sweep + -creates dict for diff_spec, these are differnetial samples that would be sampled for each + reference design + -creaste sweep_reference-dict which will contain paramters that will be sweeped over to generate reference + values + """ + sweep_params = {} + diff_params = {} + sweep_samples = 1 + min_num_samples = None + force_rerun = None + if "diff_mode" in loop_value[key]: + diff_samples = loop_value[key]["num_samples"] + diff_params[loop_value[key]["param"]] = loop_value[key] + expected_num_samples = loop_value[key]["num_samples"] + min_num_samples = loop_value[key].get("min_num_samples") + force_rerun = loop_value[key].get("rerun") + else: + expected_num_samples = loop_value.get("expected_num_samples", None) + min_num_samples = loop_value.get("min_num_samples", None) + force_rerun = loop_value.get("force_rerun", None) + for key, values in loop_value[key].items(): + if isinstance(values, dict) and "diff_mode" in values: + diff_samples = values["num_samples"] + diff_params[values["param"]] = values + sweep_samples = loop_value["sweep_reference_params"]["num_samples"] + for key, values in loop_value["sweep_reference_params"].items(): + if key != "num_samples" and key != "min_num_samples": + sweep_params[values["param"]] = values + if "num_samples" not in values: + sweep_params[values["param"]]["num_samples"] = sweep_samples + return ( + sweep_params, + sweep_samples, + diff_params, + diff_samples, + expected_num_samples, + min_num_samples, + force_rerun, + ) + + def get_sweep_params(self, key, loop_value): + if "type" in loop_value[key]: + num_samples = loop_value[key]["num_samples"] + try: + expected_num_samples = loop_value[key]["expected_num_samples"] + except KeyError: + expected_num_samples = num_samples + # try: + min_num_samples = loop_value[key].get("min_num_samples") + force_rerun = loop_value[key].get("force_rerun") + param = loop_value[key]["param"] + # except: + params = {param: loop_value[key]} + else: + params = {} + num_samples = 1 + expected_num_samples = loop_value.get("expected_num_samples", None) + min_num_samples = loop_value.get("min_num_samples", None) + force_rerun = loop_value.get("force_rerun", None) + for key, values in loop_value[key].items(): + if isinstance(values, dict) and "type" in values: + param = values["param"] + params[param] = values + num_samples = num_samples * values["num_samples"] + if expected_num_samples == None: + expected_num_samples = num_samples + return ( + params, + num_samples, + expected_num_samples, + min_num_samples, + force_rerun, + ) + + def init_sim_options(self): + """resets simulations options""" + + self.build_defaults = {} + self.init_defaults = {} + self.optimize_defaults = {} + self.sweep_params = {} + self.diff_params = {} + self.diff_samples = 0 + + self._create_save_directory(self.data_dir) + self.save_dir = self.data_dir + "/output" + + self._create_save_directory(self.save_dir) + self.save_dir = self.save_dir + "/" + self.save_name + self.h5_file_location_default = self.save_dir + self.h5_directory = "" + + def execute_sweep( + self, + sweep_directory, + ): + """runs through sweep directory and execute each simulation + unless in test mode, in which only try build, init, and solve""" + for key, value in sweep_directory.items(): + if key != "simulation_setup": + self.execute_sweep(value) + elif self.test_mode: + self.test_setup(value) + break + else: + self.execute_param_sweep_run(value) + + def execute_param_sweep_run(self, value): + """this executes the parameter sweep + if no data exists in the back upfile, or user forces exceution + this defined by check solution exists function + """ + self.setup_param_sweep(value) + solution_test = self.check_solution_exists() + if solution_test: + self.build_sim_kwargs() + self.sweep_params = value["sweep_params"] + if value["diff_params"] == {}: + self.run_parameter_sweep() + else: + self.differential_sweep_specs = value["diff_params"] + self.diff_samples = value["diff_samples"] + self.run_diff_parameter_sweep() + + def setup_param_sweep(self, value): + """set up variables before a sweep run with parmater sweep + tool, resets any of prior options""" + self.init_sim_options() + self.options = value["original_options_dict"] + self.build_default = value["build_defaults"] + self.optimize_defaults = value["optimize_defaults"] + + self.init_defaults = value["init_defaults"] + self.save_dir = value["dir"] + self.h5_directory = value["h5dir"] + val = self.h5_directory.split("/")[0] + + self.h5_file_location = ( + self.h5_file_location_default + "_analysisType_" + str(val) + ".h5" + ) + # resets it if file name changes + if ( + self.h5_backup_location is not None + and self.h5_file_location not in self.h5_backup_location + ): + self.h5_backup_location = None + self.num_samples = value["num_samples"] + self.expected_num_samples = value["expected_num_samples"] + self.force_rerun = value["force_rerun"] + self.min_num_samples = value["min_num_samples"] + + def build_sim_kwargs(self): + """here we build all the kwargs option + first loading defaults provided by default calls, followed + by those in the loops, overriding any defaults or adding new + options to kwargs""" + # check if user wants to reinit befoure sweep. + + self.initialize_before_sweep = self.options.get( + "initialize_before_sweep", False + ) + self.number_of_subprocesses = self.options.get( + "number_of_subprocesses", self.number_of_subprocesses + ) + self.parallel_back_end = self.options.get( + "parallel_back_end", self.parallel_back_end + ) + self.update_sweep_params_before_init = self.options.get( + "update_sweep_params_before_init", False + ) + self.build_outputs_kwargs = self.options.get("build_outputs_kwargs", None) + # generated combined build kwargs (default + loop) + self.combined_build_defaults = {} # self.build_default + self.combined_build_defaults.update(self.options.get("build_defaults", {})) + self.combined_build_defaults.update(self.build_default) + # generated combined optimize kwargs (default + loop) + self.combined_optimize_defaults = {} + self.combined_optimize_defaults.update( + self.options.get("optimize_defaults", {}) + ) + self.combined_optimize_defaults.update(self.optimize_defaults) + # generated combined init kwargs (default + loop) + self.combined_init_defaults = {} + self.combined_init_defaults.update(self.options.get("init_defaults", {})) + self.combined_init_defaults.update(self.init_defaults) + + def _check_solution_exists(self): + """hidden function to check if solution + exists, and create and h5 file for data storage""" + self.h5_backup_location = create_backup_file( + self.h5_file_location, self.h5_backup_location, self.h5_directory + ) + create_h5_file(self.h5_file_location) + + sucess_feasible = self.expected_num_samples + run_sweep = merge_data_into_file( + self.h5_file_location, + self.h5_backup_location, + self.h5_directory, + expected_solved_values=self.expected_num_samples, + min_solve_values=self.min_num_samples, + force_rerun=self.force_rerun, + ) + return run_sweep + + def check_solution_exists(self): + """check if solution exists, ensuring to use only + rank 0 if user running MPI, otherwise do direct + solution check""" + self.cur_h5_file = (self.h5_file_location, self.h5_directory) + if has_mpi_peer_processes(): + mpi_comm = get_mpi_comm_process() + results = np.empty(mpi_comm.Get_size(), dtype=bool) + + results[:] = True + if mpi_comm.Get_rank() == 0: + success = self._check_solution_exists() + results[:] = success + + mpi_comm.Bcast(results, root=0) + success = results[mpi_comm.Get_rank()] + else: + success = self._check_solution_exists() + return success + + def _create_save_directory(self, save_dir): + """used to create a save directory (outputs)""" + try: + os.mkdir(save_dir) + except OSError as error: + pass + + def _default_build_ouptuts(self, model, output_keys): + outputs = {} + for key, pyo_object in output_keys.items(): + outputs[key] = model.find_component(pyo_object) + return outputs + + def run_parameter_sweep(self): + """setup and run paramer sweep""" + + # get solver if not provided by user + if self.solver is None: + solver = get_solver() + else: + solver = self.solver + + # add solver to init kwargs + self.combined_init_defaults.update({"solver": solver}) + # add solver to optimize kwarg + self.combined_optimize_defaults.update({"solver": solver}) + + # setup parameter sweep tool + ps_kwargs = {} + ps_kwargs["csv_results_file_name"] = None + ps_kwargs["h5_results_file_name"] = self.cur_h5_file[0] + ps_kwargs["h5_parent_group_name"] = self.cur_h5_file[1] + + ps_kwargs["optimize_function"] = self.optimize_function + ps_kwargs["optimize_kwargs"] = self.combined_optimize_defaults + + ps_kwargs["initialize_function"] = self.initialize_function + ps_kwargs["initialize_kwargs"] = self.combined_init_defaults + ps_kwargs["initialize_before_sweep"] = self.initialize_before_sweep + + ps_kwargs[ + "update_sweep_params_before_init" + ] = self.update_sweep_params_before_init + + ps_kwargs["custom_do_param_sweep"] = self.custom_do_param_sweep + ps_kwargs["custom_do_param_sweep_kwargs"] = self.custom_do_param_sweep_kwargs + + ps_kwargs["probe_function"] = self.probe_function + if self.build_outputs_kwargs is not None and self.build_outputs == None: + ps_kwargs["build_outputs"] = self._default_build_ouptuts + ps_kwargs["build_outputs_kwargs"] = { + "output_keys": self.build_outputs_kwargs + } + else: + ps_kwargs["build_outputs"] = self.build_outputs + ps_kwargs["build_outputs_kwargs"] = self.build_outputs_kwargs + + ps_kwargs["number_of_subprocesses"] = self.number_of_subprocesses + ps_kwargs["parallel_back_end"] = self.parallel_back_end + + ps = ParameterSweep(**ps_kwargs) + ps.parameter_sweep( + self.build_function, + ParameterSweepReader()._dict_to_params, + build_outputs=ps_kwargs["build_outputs"], + build_outputs_kwargs=ps_kwargs["build_outputs_kwargs"], + num_samples=self.num_samples, + build_model_kwargs=self.combined_build_defaults, + build_sweep_params_kwargs={"input_dict": self.sweep_params}, + ) + + def run_diff_parameter_sweep(self): + """setup and run diff paramer sweep + fix once the diff paramter tool is updated to new version.""" + + if self.solver is None: + solver = get_solver() + else: + solver = self.solver + + # add solver to init kwargs + self.combined_init_defaults.update({"solver": solver}) + # add solver to optimize kwarg + self.combined_optimize_defaults.update({"solver": solver}) + + # legacy, need to build m + m = self.build_function(**self.combined_build_defaults) + + # setup parmater sweep tool + ps_kwargs = {} + ps_kwargs["csv_results_file_name"] = None + ps_kwargs["h5_results_file_name"] = self.cur_h5_file[0] + ps_kwargs["h5_parent_group_name"] = self.cur_h5_file[1] + + ps_kwargs["optimize_function"] = self.optimize_function + ps_kwargs["optimize_kwargs"] = self.combined_optimize_defaults + + ps_kwargs["initialize_function"] = self.initialize_function + ps_kwargs["initialize_kwargs"] = self.combined_init_defaults + ps_kwargs["initialize_before_sweep"] = self.initialize_before_sweep + + ps_kwargs[ + "update_sweep_params_before_init" + ] = self.update_sweep_params_before_init + + ps_kwargs["custom_do_param_sweep"] = self.custom_do_param_sweep + ps_kwargs["custom_do_param_sweep_kwargs"] = self.custom_do_param_sweep_kwargs + + ps_kwargs["probe_function"] = self.probe_function + + ps_kwargs["number_of_subprocesses"] = self.number_of_subprocesses + ps_kwargs["parallel_back_end"] = self.parallel_back_end + + if self.build_outputs_kwargs is not None and self.build_outputs == None: + ps_kwargs["build_outputs"] = self._default_build_ouptuts + ps_kwargs["build_outputs_kwargs"] = { + "output_keys": self.build_outputs_kwargs + } + else: + ps_kwargs["build_outputs"] = self.build_outputs + ps_kwargs["build_outputs_kwargs"] = self.build_outputs_kwargs + + ps_kwargs[ + "differential_sweep_specs" + ] = ParameterSweepReader()._dict_to_diff_spec(m, self.differential_sweep_specs) + ps = DifferentialParameterSweep(**ps_kwargs) + + ps.parameter_sweep( + self.build_function, + ParameterSweepReader()._dict_to_params, + num_samples=self.num_samples, + build_model_kwargs=self.combined_build_defaults, + build_sweep_params_kwargs={"input_dict": self.sweep_params}, + build_outputs=ps_kwargs["build_outputs"], + build_outputs_kwargs=ps_kwargs["build_outputs_kwargs"], + ) diff --git a/watertap/tools/analysis_tools/loop_tool/tests/ro_setup.py b/watertap/tools/analysis_tools/loop_tool/tests/ro_setup.py new file mode 100644 index 0000000000..ded3dc9b5a --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/ro_setup.py @@ -0,0 +1,44 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# + +import watertap.examples.flowsheets.RO_with_energy_recovery.RO_with_energy_recovery as ro_erd +from watertap.tools.parameter_sweep.parameter_sweep import ( + ParameterSweep, + RecursiveParameterSweep, +) +from watertap.tools.parameter_sweep.parameter_sweep_reader import ParameterSweepReader + +from watertap.tools.parameter_sweep.parameter_sweep_differential import ( + DifferentialParameterSweep, +) +from idaes.core.solvers import get_solver +import os + +__author__ = "Alexander V. Dudchenko (SLAC)" + + +def ro_build(**kwargs): + m = ro_erd.build(**kwargs) + return m + + +def ro_init(m, solver=None, **kwargs): + ro_erd.set_operating_conditions( + m, water_recovery=0.5, over_pressure=0.3, solver=solver + ) + ro_erd.initialize_system(m, solver=solver) + ro_erd.optimize_set_up(m) + + +def ro_solve(m, solver=None, **kwargs): + result = solver.solve(m) + return result diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_all_options.yaml b/watertap/tools/analysis_tools/loop_tool/tests/test_all_options.yaml new file mode 100644 index 0000000000..f10089354e --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_all_options.yaml @@ -0,0 +1,104 @@ +sweep_tests: + initialize_before_sweep: True + number_of_subprocesses: 10 + parallel_back_end: 'RayIo' + update_sweep_params_before_init: True + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + init_loop: + sim_cases: + case_a: + erd_type: pump_as_turbine + case_b: + erd_type: pressure_exchanger + sweep_param_loop: + membrane_cost: + type: LinearSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 20 + upper_limit: 30 + num_samples: 3 + min_num_samples: 10 + expected_num_samples: 20 + force_rerun: True + membrane_group: + membrane_cost: + type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 25 + upper_limit: 30 + num_samples: 2 + factor_membrane_replacement: + type: UniformSample + param: fs.costing.reverse_osmosis.factor_membrane_replacement + lower_limit: 0.15 + upper_limit: 0.2 + num_samples: 2 + min_num_samples: 10 + expected_num_samples: 20 + force_rerun: True + +diff_tests: + initialize_before_sweep: True + number_of_subprocesses: 10 + parallel_back_end: 'RayIo' + update_sweep_params_before_init: True + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + diff_param_loop: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + min_num_samples: 10 + expected_num_samples: 20 + force_rerun: True + membrane_group: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + min_num_samples: 1 + expected_num_samples: 1 + force_rerun: True + sweep_reference_params: + membrane_cost: + type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 25 + upper_limit: 30 + factor_membrane_replacement: + type: UniformSample + param: fs.costing.reverse_osmosis.factor_membrane_replacement + lower_limit: 0.15 + upper_limit: 0.2 + num_samples: 2 + + \ No newline at end of file diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_bad_default.yaml b/watertap/tools/analysis_tools/loop_tool/tests/test_bad_default.yaml new file mode 100644 index 0000000000..abf082e625 --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_bad_default.yaml @@ -0,0 +1,31 @@ +ro_analysis: + initialize_before_sweep: False + random_key: True + build_defaults: + erd_type: pump_as_turbine + build_outputs_kwargs: + LCOW: fs.costing.LCOW + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + sweep_param_loop: + membrane_cost: + type: LinearSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 20 + upper_limit: 30 + num_samples: 3 + membrane_group: + membrane_cost: + type: LinearSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 25 + upper_limit: 30 + num_samples: 2 + factor_membrane_replacement: + type: LinearSample + param: fs.costing.reverse_osmosis.factor_membrane_replacement + lower_limit: 0.15 + upper_limit: 0.2 + num_samples: 2 \ No newline at end of file diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_data_merging_tool.py b/watertap/tools/analysis_tools/loop_tool/tests/test_data_merging_tool.py new file mode 100644 index 0000000000..1fc2d0893c --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_data_merging_tool.py @@ -0,0 +1,119 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +import pytest +import os +import numpy as np + +from watertap.tools.analysis_tools.loop_tool import ( + data_merging_tool, +) +import h5py + +__author__ = "Alexander V. Dudchenko (SLAC)" + +_this_file_path = os.path.dirname(os.path.abspath(__file__)) + + +def create_test_h5_file(): + f_name = _this_file_path + "/test_h5_file.h5" + test_group = "test_vals" + h5file = h5py.File(f_name, "w") + h5file[test_group + "/solve_successful/solve_successful"] = np.ones(10) + h5file.close() + return f_name, test_group + # test creating new file, no back exists, so we should run sweep + + +@pytest.mark.component +def test_new_file_run(): + """test create new file, and ensure we run a sweep""" + h5_file, test_group = create_test_h5_file() + run_sweep = data_merging_tool.merge_data_into_file( + h5_file, + None, + test_group, + expected_solved_values=10, + min_solve_values=10, + force_rerun=None, + ) + try: + os.remove(h5_file) + except OSError: + pass + assert run_sweep == True + + +@pytest.mark.component +def test_runs_from_backup(): + h5_file, test_group = create_test_h5_file() + backup_name = data_merging_tool.create_backup_file(h5_file, None, "test_vals") + run_sweep = data_merging_tool.merge_data_into_file( + h5_file, + backup_name, + test_group, + expected_solved_values=10, + min_solve_values=None, + force_rerun=None, + ) + # solutions shouild already be present in solved file, so we re not + # reruning + assert run_sweep == False + + # we expect more solutions then present in file, so + # run_sweep shold be true + run_sweep = data_merging_tool.merge_data_into_file( + h5_file, + backup_name, + test_group, + expected_solved_values=20, + min_solve_values=None, + force_rerun=None, + ) + assert run_sweep == True + + # we expect min 9 solutions, so should not re-run as we have 10 + run_sweep = data_merging_tool.merge_data_into_file( + h5_file, + backup_name, + test_group, + expected_solved_values=20, + min_solve_values=9, + force_rerun=None, + ) + assert run_sweep == False + + # We do not want to re-run, so should not run_sweep + run_sweep = data_merging_tool.merge_data_into_file( + h5_file, + backup_name, + test_group, + expected_solved_values=20, + min_solve_values=10, + force_rerun=False, + ) + assert run_sweep == False + + # We are forcing a re-run even thouhg there as many values as expected + run_sweep = data_merging_tool.merge_data_into_file( + h5_file, + backup_name, + test_group, + expected_solved_values=10, + min_solve_values=10, + force_rerun=True, + ) + assert run_sweep == False + try: + os.remove(h5_file) + os.remove(backup_name) + except OSError: + pass diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_diff.yaml b/watertap/tools/analysis_tools/loop_tool/tests/test_diff.yaml new file mode 100644 index 0000000000..421c743a0f --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_diff.yaml @@ -0,0 +1,45 @@ +ro_diff_analysis: + initialize_before_sweep: False + build_defaults: + erd_type: pump_as_turbine + diff_param_loop: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + membrane_group: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + nominal_lb: 0.15 + nominal_ub: 0.2 + num_samples: 1 + sweep_reference_params: + membrane_cost: + type: UniformSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 25 + upper_limit: 30 + factor_membrane_replacement: + type: UniformSample + param: fs.costing.reverse_osmosis.factor_membrane_replacement + lower_limit: 0.15 + upper_limit: 0.2 + num_samples: 2 diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_expected_diff_directory.yaml b/watertap/tools/analysis_tools/loop_tool/tests/test_expected_diff_directory.yaml new file mode 100644 index 0000000000..43ddbe7432 --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_expected_diff_directory.yaml @@ -0,0 +1,172 @@ +ro_diff_analysis: + membrane_cost: + simulation_setup: + build_defaults: {} + diff_params: + fs.costing.reverse_osmosis.membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + diff_samples: 1 + expected_num_samples: 1 + force_rerun: null + h5dir: ro_diff_analysis/membrane_cost + init_defaults: {} + min_num_samples: null + num_samples: 2 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + diff_param_loop: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + membrane_group: + factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + sweep_reference_params: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + num_samples: 2 + initialize_before_sweep: false + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + membrane_group: + simulation_setup: + build_defaults: {} + diff_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + fs.costing.reverse_osmosis.membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + diff_samples: 1 + expected_num_samples: null + force_rerun: null + h5dir: ro_diff_analysis/membrane_group + init_defaults: {} + min_num_samples: null + num_samples: 2 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + diff_param_loop: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + membrane_group: + factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + sweep_reference_params: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + num_samples: 2 + initialize_before_sweep: false + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_expected_option_directory.yaml b/watertap/tools/analysis_tools/loop_tool/tests/test_expected_option_directory.yaml new file mode 100644 index 0000000000..195cfab690 --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_expected_option_directory.yaml @@ -0,0 +1,966 @@ +diff_tests: + erd_type: + pressure_exchanger: + membrane_cost: + simulation_setup: + build_defaults: + erd_type: pressure_exchanger + diff_params: + fs.costing.reverse_osmosis.membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + expected_num_samples: 20 + force_rerun: true + min_num_samples: 10 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + diff_samples: 1 + expected_num_samples: 1 + force_rerun: null + h5dir: diff_tests/erd_type/pressure_exchanger/membrane_cost + init_defaults: {} + min_num_samples: 10 + num_samples: 2 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + diff_param_loop: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + expected_num_samples: 20 + force_rerun: true + min_num_samples: 10 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + membrane_group: + expected_num_samples: 1 + factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + force_rerun: true + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + min_num_samples: 1 + sweep_reference_params: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + num_samples: 2 + erd_type: + - pump_as_turbine + - pressure_exchanger + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + membrane_group: + simulation_setup: + build_defaults: + erd_type: pressure_exchanger + diff_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + fs.costing.reverse_osmosis.membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + diff_samples: 1 + expected_num_samples: null + force_rerun: null + h5dir: diff_tests/erd_type/pressure_exchanger/membrane_group + init_defaults: {} + min_num_samples: null + num_samples: 2 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + diff_param_loop: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + expected_num_samples: 20 + force_rerun: true + min_num_samples: 10 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + membrane_group: + expected_num_samples: 1 + factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + force_rerun: true + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + min_num_samples: 1 + sweep_reference_params: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + num_samples: 2 + erd_type: + - pump_as_turbine + - pressure_exchanger + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + pump_as_turbine: + membrane_cost: + simulation_setup: + build_defaults: + erd_type: pump_as_turbine + diff_params: + fs.costing.reverse_osmosis.membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + expected_num_samples: 20 + force_rerun: true + min_num_samples: 10 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + diff_samples: 1 + expected_num_samples: 1 + force_rerun: null + h5dir: diff_tests/erd_type/pump_as_turbine/membrane_cost + init_defaults: {} + min_num_samples: 10 + num_samples: 2 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + diff_param_loop: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + expected_num_samples: 20 + force_rerun: true + min_num_samples: 10 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + membrane_group: + expected_num_samples: 1 + factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + force_rerun: true + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + min_num_samples: 1 + sweep_reference_params: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + num_samples: 2 + erd_type: + - pump_as_turbine + - pressure_exchanger + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + membrane_group: + simulation_setup: + build_defaults: + erd_type: pump_as_turbine + diff_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + fs.costing.reverse_osmosis.membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + diff_samples: 1 + expected_num_samples: null + force_rerun: null + h5dir: diff_tests/erd_type/pump_as_turbine/membrane_group + init_defaults: {} + min_num_samples: null + num_samples: 2 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + diff_param_loop: + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + expected_num_samples: 20 + force_rerun: true + min_num_samples: 10 + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + membrane_group: + expected_num_samples: 1 + factor_membrane_replacement: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 0.15 + nominal_ub: 300.2 + num_samples: 1 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + relative_lb: -0.01 + relative_ub: -0.01 + force_rerun: true + membrane_cost: + diff_mode: percentile + diff_sample_type: UniformSample + nominal_lb: 25 + nominal_ub: 30 + num_samples: 1 + param: fs.costing.reverse_osmosis.membrane_cost + relative_lb: -0.01 + relative_ub: -0.01 + min_num_samples: 1 + sweep_reference_params: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + num_samples: 2 + erd_type: + - pump_as_turbine + - pressure_exchanger + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 +sweep_tests: + erd_type: + pressure_exchanger: + sim_cases: + case_a: + membrane_cost: + simulation_setup: + build_defaults: + erd_type: pressure_exchanger + diff_params: {} + diff_samples: 0 + expected_num_samples: 20 + force_rerun: true + h5dir: sweep_tests/erd_type/pressure_exchanger/sim_cases/case_a/membrane_cost + init_defaults: + erd_type: pump_as_turbine + min_num_samples: 10 + num_samples: 3 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + init_loop: + sim_cases: + case_a: + erd_type: pump_as_turbine + case_b: + erd_type: pressure_exchanger + sweep_param_loop: + membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + expected_num_samples: 20 + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + force_rerun: true + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + min_num_samples: 10 + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + simulation_setup: + build_defaults: + erd_type: pressure_exchanger + diff_params: {} + diff_samples: 0 + expected_num_samples: 4 + force_rerun: null + h5dir: sweep_tests/erd_type/pressure_exchanger/sim_cases/case_a/membrane_group + init_defaults: + erd_type: pump_as_turbine + min_num_samples: null + num_samples: 4 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + init_loop: + sim_cases: + case_a: + erd_type: pump_as_turbine + case_b: + erd_type: pressure_exchanger + sweep_param_loop: + membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + expected_num_samples: 20 + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + force_rerun: true + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + min_num_samples: 10 + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + case_b: + membrane_cost: + simulation_setup: + build_defaults: + erd_type: pressure_exchanger + diff_params: {} + diff_samples: 0 + expected_num_samples: 20 + force_rerun: true + h5dir: sweep_tests/erd_type/pressure_exchanger/sim_cases/case_b/membrane_cost + init_defaults: + erd_type: pressure_exchanger + min_num_samples: 10 + num_samples: 3 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + init_loop: + sim_cases: + case_a: + erd_type: pump_as_turbine + case_b: + erd_type: pressure_exchanger + sweep_param_loop: + membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + expected_num_samples: 20 + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + force_rerun: true + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + min_num_samples: 10 + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + simulation_setup: + build_defaults: + erd_type: pressure_exchanger + diff_params: {} + diff_samples: 0 + expected_num_samples: 4 + force_rerun: null + h5dir: sweep_tests/erd_type/pressure_exchanger/sim_cases/case_b/membrane_group + init_defaults: + erd_type: pressure_exchanger + min_num_samples: null + num_samples: 4 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + init_loop: + sim_cases: + case_a: + erd_type: pump_as_turbine + case_b: + erd_type: pressure_exchanger + sweep_param_loop: + membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + expected_num_samples: 20 + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + force_rerun: true + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + min_num_samples: 10 + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + pump_as_turbine: + sim_cases: + case_a: + membrane_cost: + simulation_setup: + build_defaults: + erd_type: pump_as_turbine + diff_params: {} + diff_samples: 0 + expected_num_samples: 20 + force_rerun: true + h5dir: sweep_tests/erd_type/pump_as_turbine/sim_cases/case_a/membrane_cost + init_defaults: + erd_type: pump_as_turbine + min_num_samples: 10 + num_samples: 3 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + init_loop: + sim_cases: + case_a: + erd_type: pump_as_turbine + case_b: + erd_type: pressure_exchanger + sweep_param_loop: + membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + expected_num_samples: 20 + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + force_rerun: true + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + min_num_samples: 10 + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + simulation_setup: + build_defaults: + erd_type: pump_as_turbine + diff_params: {} + diff_samples: 0 + expected_num_samples: 4 + force_rerun: null + h5dir: sweep_tests/erd_type/pump_as_turbine/sim_cases/case_a/membrane_group + init_defaults: + erd_type: pump_as_turbine + min_num_samples: null + num_samples: 4 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + init_loop: + sim_cases: + case_a: + erd_type: pump_as_turbine + case_b: + erd_type: pressure_exchanger + sweep_param_loop: + membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + expected_num_samples: 20 + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + force_rerun: true + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + min_num_samples: 10 + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + case_b: + membrane_cost: + simulation_setup: + build_defaults: + erd_type: pump_as_turbine + diff_params: {} + diff_samples: 0 + expected_num_samples: 20 + force_rerun: true + h5dir: sweep_tests/erd_type/pump_as_turbine/sim_cases/case_b/membrane_cost + init_defaults: + erd_type: pressure_exchanger + min_num_samples: 10 + num_samples: 3 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + init_loop: + sim_cases: + case_a: + erd_type: pump_as_turbine + case_b: + erd_type: pressure_exchanger + sweep_param_loop: + membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + expected_num_samples: 20 + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + force_rerun: true + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + min_num_samples: 10 + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + simulation_setup: + build_defaults: + erd_type: pump_as_turbine + diff_params: {} + diff_samples: 0 + expected_num_samples: 4 + force_rerun: null + h5dir: sweep_tests/erd_type/pump_as_turbine/sim_cases/case_b/membrane_group + init_defaults: + erd_type: pressure_exchanger + min_num_samples: null + num_samples: 4 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + init_loop: + sim_cases: + case_a: + erd_type: pump_as_turbine + case_b: + erd_type: pressure_exchanger + sweep_param_loop: + membrane_cost: + expected_num_samples: 20 + force_rerun: true + lower_limit: 20 + min_num_samples: 10 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + expected_num_samples: 20 + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + force_rerun: true + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 + min_num_samples: 10 + initialize_before_sweep: true + number_of_subprocesses: 10 + parallel_back_end: RayIo + update_sweep_params_before_init: true + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: UniformSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: UniformSample + upper_limit: 30 diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_expected_sweep_directory.yaml b/watertap/tools/analysis_tools/loop_tool/tests/test_expected_sweep_directory.yaml new file mode 100644 index 0000000000..1d70bbc1c1 --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_expected_sweep_directory.yaml @@ -0,0 +1,218 @@ +ro_analysis: + erd_type: + pressure_exchanger: + membrane_cost: + simulation_setup: + build_defaults: + erd_type: pressure_exchanger + diff_params: {} + diff_samples: 0 + expected_num_samples: 3 + force_rerun: null + h5dir: ro_analysis/erd_type/pressure_exchanger/membrane_cost + init_defaults: {} + min_num_samples: null + num_samples: 3 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + sweep_param_loop: + membrane_cost: + lower_limit: 20 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: LinearSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + build_outputs_kwargs: + LCOW: fs.costing.LCOW + initialize_before_sweep: false + sweep_params: + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 20 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + simulation_setup: + build_defaults: + erd_type: pressure_exchanger + diff_params: {} + diff_samples: 0 + expected_num_samples: 4 + force_rerun: null + h5dir: ro_analysis/erd_type/pressure_exchanger/membrane_group + init_defaults: {} + min_num_samples: null + num_samples: 4 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + sweep_param_loop: + membrane_cost: + lower_limit: 20 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: LinearSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + build_outputs_kwargs: + LCOW: fs.costing.LCOW + initialize_before_sweep: false + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: LinearSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + pump_as_turbine: + membrane_cost: + simulation_setup: + build_defaults: + erd_type: pump_as_turbine + diff_params: {} + diff_samples: 0 + dir: D:\OneDrive\NAWI_work\Analysis\WaterTAP\watertap-dev\watertap\tools\analysis_tools\loop_tool\tests/output/ro_with_erd/ro_analysis/erd_type/pump_as_turbine/membrane_cost + expected_num_samples: 3 + force_rerun: null + h5dir: ro_analysis/erd_type/pump_as_turbine/membrane_cost + init_defaults: {} + min_num_samples: null + num_samples: 3 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + sweep_param_loop: + membrane_cost: + lower_limit: 20 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: LinearSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + build_outputs_kwargs: + LCOW: fs.costing.LCOW + initialize_before_sweep: false + sweep_params: + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 20 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + simulation_setup: + build_defaults: + erd_type: pump_as_turbine + diff_params: {} + diff_samples: 0 + dir: D:\OneDrive\NAWI_work\Analysis\WaterTAP\watertap-dev\watertap\tools\analysis_tools\loop_tool\tests/output/ro_with_erd/ro_analysis/erd_type/pump_as_turbine/membrane_group + expected_num_samples: 4 + force_rerun: null + h5dir: ro_analysis/erd_type/pump_as_turbine/membrane_group + init_defaults: {} + min_num_samples: null + num_samples: 4 + optimize_defaults: {} + original_options_dict: + build_defaults: + erd_type: pump_as_turbine + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + sweep_param_loop: + membrane_cost: + lower_limit: 20 + num_samples: 3 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + membrane_group: + factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: LinearSample + upper_limit: 0.2 + membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 + build_outputs_kwargs: + LCOW: fs.costing.LCOW + initialize_before_sweep: false + sweep_params: + fs.costing.reverse_osmosis.factor_membrane_replacement: + lower_limit: 0.15 + num_samples: 2 + param: fs.costing.reverse_osmosis.factor_membrane_replacement + type: LinearSample + upper_limit: 0.2 + fs.costing.reverse_osmosis.membrane_cost: + lower_limit: 25 + num_samples: 2 + param: fs.costing.reverse_osmosis.membrane_cost + type: LinearSample + upper_limit: 30 diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_loop_tool.py b/watertap/tools/analysis_tools/loop_tool/tests/test_loop_tool.py new file mode 100644 index 0000000000..25274b55a0 --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_loop_tool.py @@ -0,0 +1,332 @@ +################################################################################# +# WaterTAP Copyright (c) 2020-2023, The Regents of the University of California, +# through Lawrence Berkeley National Laboratory, Oak Ridge National Laboratory, +# National Renewable Energy Laboratory, and National Energy Technology +# Laboratory (subject to receipt of any required approvals from the U.S. Dept. +# of Energy). All rights reserved. +# +# Please see the files COPYRIGHT.md and LICENSE.md for full copyright and license +# information, respectively. These files are also available online at the URL +# "https://github.com/watertap-org/watertap/" +################################################################################# +import pytest +import os +import numpy as np + +from watertap.tools.analysis_tools.loop_tool.tests import ro_setup +from watertap.tools.analysis_tools.loop_tool.loop_tool import loopTool, get_working_dir +import yaml +import h5py + +from watertap.tools.parallel.parallel_manager_factory import ( + has_mpi_peer_processes, + get_mpi_comm_process, +) + +__author__ = "Alexander V. Dudchenko (SLAC)" + +_this_file_path = os.path.dirname(os.path.abspath(__file__)) + + +def diff_dict_check(dicta, dictb): + for key in dicta: + if key != "dir": + if isinstance(dicta[key], dict): + diff_dict_check(dicta[key], dictb[key]) + + elif dicta[key] != dictb[key]: + return False + return True + + +@pytest.fixture() +def loop_test_options_setup(): + lp = loopTool( + _this_file_path + "/test_all_options.yaml", + build_function=ro_setup.ro_build, + initialize_function=ro_setup.ro_init, + optimize_function=ro_setup.ro_solve, + saving_dir=_this_file_path, + save_name="ro_with_erd", + execute_simulations=False, + number_of_subprocesses=1, + ) + lp.build_run_dict() + """ used to generate test file""" + # with open("test_expected_option_directory.yaml", "w") as file: + # documents = yaml.dump(lp.sweep_directory, file) + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + with open( + _this_file_path + "/test_expected_option_directory.yaml", "r" + ) as infile: + expected_run_dict = yaml.safe_load(infile) + else: + expected_run_dict = None + return lp, expected_run_dict + + +@pytest.fixture() +def loop_sweep_setup(): + lp = loopTool( + _this_file_path + "/test_sweep.yaml", + build_function=ro_setup.ro_build, + initialize_function=ro_setup.ro_init, + optimize_function=ro_setup.ro_solve, + saving_dir=_this_file_path, + save_name="ro_with_erd", + execute_simulations=False, + number_of_subprocesses=1, + ) + lp.build_run_dict() + """ used to generate test file""" + # with open("test_expected_sweep_directory.yaml", "w") as file: + # documents = yaml.dump(lp.sweep_directory, file) + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + with open( + _this_file_path + "/test_expected_sweep_directory.yaml", "r" + ) as infile: + expected_run_dict = yaml.safe_load(infile) + else: + expected_run_dict = None + return lp, expected_run_dict + + +@pytest.fixture() +def loop_diff_setup(): + lp = loopTool( + _this_file_path + "/test_diff.yaml", + build_function=ro_setup.ro_build, + initialize_function=ro_setup.ro_init, + optimize_function=ro_setup.ro_solve, + saving_dir=_this_file_path, + save_name="ro_with_erd", + execute_simulations=False, + number_of_subprocesses=1, + ) + lp.build_run_dict() + """ used to generate test file""" + # with open("test_expected_diff_directory.yaml", "w") as file: + # documents = yaml.dump(lp.sweep_directory, file) + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + with open( + _this_file_path + "/test_expected_diff_directory.yaml", "r" + ) as infile: + expected_run_dict = yaml.safe_load(infile) + else: + expected_run_dict = None + return lp, expected_run_dict + + +@pytest.mark.component +def test_failed_setup(): + with pytest.raises(KeyError, match=r"Unsupported key random_key"): + lp = loopTool( + _this_file_path + "/test_bad_default.yaml", + build_function=ro_setup.ro_build, + initialize_function=ro_setup.ro_init, + optimize_function=ro_setup.ro_solve, + saving_dir=_this_file_path, + save_name="ro_with_erd", + execute_simulations=False, + number_of_subprocesses=1, + ) + lp.build_run_dict() + with pytest.raises( + KeyError, match=r"sweep_param_loop or diff_param_loop not found in config file!" + ): + lp = loopTool( + _this_file_path + "/test_missing_sweep_loop.yaml", + build_function=ro_setup.ro_build, + initialize_function=ro_setup.ro_init, + optimize_function=ro_setup.ro_solve, + saving_dir=_this_file_path, + save_name="ro_with_erd", + execute_simulations=False, + number_of_subprocesses=1, + ) + lp.build_run_dict() + + +@pytest.mark.component +def test_options_setups(loop_test_options_setup): + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + lp, expected_run_dict = loop_test_options_setup + lp.build_run_dict() + + assert diff_dict_check(lp.sweep_directory, expected_run_dict) + + +@pytest.mark.component +def test_sweep_setup(loop_sweep_setup): + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + lp, expected_run_dict = loop_sweep_setup + lp.build_run_dict() + + assert diff_dict_check(lp.sweep_directory, expected_run_dict) + + +@pytest.mark.component +def test_diff_setup(loop_diff_setup): + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + lp, expected_run_dict = loop_diff_setup + lp.build_run_dict() + + assert diff_dict_check(lp.sweep_directory, expected_run_dict) + + +@pytest.mark.component +def test_sweep_run(loop_sweep_setup): + lp, test_file = loop_sweep_setup + lp.build_run_dict() + # remove any existing file before test + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + if os.path.isfile(lp.h5_file_location_default + "_analysisType_ro_analysis.h5"): + os.remove(lp.h5_file_location_default + "_analysisType_ro_analysis.h5") + + lp.run_simulations() + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + h5file = h5py.File( + lp.h5_file_location_default + "_analysisType_ro_analysis.h5", "r" + ) + data = h5file[ + "ro_analysis/erd_type/pressure_exchanger/membrane_cost/outputs/LCOW/value" + ] + + true_vals = [0.37203417, 0.39167574, 0.41117995] + d = data[()] + # print(true_vals, d) + for i, tv in enumerate(true_vals): + assert d[i] == pytest.approx(tv, rel=1e-2) + data = h5file[ + "ro_analysis/erd_type/pump_as_turbine/membrane_cost/outputs/LCOW/value" + ] + + true_vals = [0.50886109, 0.52850266, 0.54814424] + d = data[()] + # print(true_vals, d) + for i, tv in enumerate(true_vals): + assert d[i] == pytest.approx(tv, rel=1e-2) + data = h5file[ + "ro_analysis/erd_type/pressure_exchanger/membrane_group/outputs/LCOW/value" + ] + + true_vals = [ + 0.3810009713006634, + 0.3916757385992817, + 0.3985075912766517, + 0.4111799488092862, + ] + d = data[()] + # print(true_vals, d) + for i, tv in enumerate(true_vals): + assert d[i] == pytest.approx(tv, rel=1e-2) + data = h5file[ + "ro_analysis/erd_type/pump_as_turbine/membrane_group/outputs/LCOW/value" + ] + + true_vals = [ + 0.5178278972844322, + 0.5285026645830471, + 0.5353345156541605, + 0.5481442364124981, + ] + d = data[()] + # print(true_vals, d) + for i, tv in enumerate(true_vals): + assert d[i] == pytest.approx(tv, rel=1e-2) + h5file.close() + + +@pytest.mark.component +def test_sweep_backup(loop_sweep_setup): + """test that backup works, will not run actual simulation ,create a back up file, and + load data from it into sim file. The lp.back_file_name should not be None + """ + lp, test_file = loop_sweep_setup + lp.build_run_dict() + lp.run_simulations() + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + assert lp.h5_backup_location != None + h5file = h5py.File( + lp.h5_file_location_default + "_analysisType_ro_analysis.h5", "r" + ) + data = h5file[ + "ro_analysis/erd_type/pressure_exchanger/membrane_cost/outputs/LCOW/value" + ] + + true_vals = [0.37203417, 0.39167574, 0.41117995] + d = data[()] + for i, tv in enumerate(true_vals): + assert d[i] == pytest.approx(tv, rel=1e-2) + data = h5file[ + "ro_analysis/erd_type/pump_as_turbine/membrane_cost/outputs/LCOW/value" + ] + true_vals = [0.50886109, 0.52850266, 0.54814424] + d = data[()] + + for i, tv in enumerate(true_vals): + assert d[i] == pytest.approx(tv, rel=1e-2) + h5file.close() + + os.remove(lp.h5_file_location_default + "_analysisType_ro_analysis.h5") + # try: + os.remove(lp.h5_backup_location) + + +@pytest.mark.component +def test_diff_run(loop_diff_setup): + lp, test_file = loop_diff_setup + lp.build_run_dict() + # clean up any files from prior failed test + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + if os.path.isfile( + lp.h5_file_location_default + "_analysisType_ro_diff_analysis.h5" + ): + os.remove(lp.h5_file_location_default + "_analysisType_ro_diff_analysis.h5") + lp.run_simulations() + if has_mpi_peer_processes() == False or ( + has_mpi_peer_processes() and get_mpi_comm_process().Get_rank() == 0 + ): + h5file = h5py.File( + lp.h5_file_location_default + "_analysisType_ro_diff_analysis.h5", "r" + ) + + data = h5file["ro_diff_analysis/membrane_cost/outputs/fs.costing.LCOW/value"][ + () + ] + + # for i, tv in enumerate(true_vals): + assert len(data) == 4 + + data_a = h5file[ + "ro_diff_analysis/membrane_group/sweep_params/fs.costing.reverse_osmosis.factor_membrane_replacement/value" + ][()] + data_b = h5file[ + "ro_diff_analysis/membrane_group/sweep_params/fs.costing.reverse_osmosis.membrane_cost/value" + ][()] + # for i, tv in enumerate(true_vals): + assert len(data_a) == 4 + assert len(data_b) == 4 + h5file.close() + # try: + os.remove(lp.h5_file_location_default + "_analysisType_ro_diff_analysis.h5") diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_missing_sweep_loop.yaml b/watertap/tools/analysis_tools/loop_tool/tests/test_missing_sweep_loop.yaml new file mode 100644 index 0000000000..d32674ca2d --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_missing_sweep_loop.yaml @@ -0,0 +1,10 @@ +ro_analysis: + initialize_before_sweep: False + build_defaults: + erd_type: pump_as_turbine + build_outputs_kwargs: + LCOW: fs.costing.LCOW + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger diff --git a/watertap/tools/analysis_tools/loop_tool/tests/test_sweep.yaml b/watertap/tools/analysis_tools/loop_tool/tests/test_sweep.yaml new file mode 100644 index 0000000000..aa165f830a --- /dev/null +++ b/watertap/tools/analysis_tools/loop_tool/tests/test_sweep.yaml @@ -0,0 +1,30 @@ +ro_analysis: + initialize_before_sweep: False + build_defaults: + erd_type: pump_as_turbine + build_outputs_kwargs: + LCOW: fs.costing.LCOW + build_loop: + erd_type: + - pump_as_turbine + - pressure_exchanger + sweep_param_loop: + membrane_cost: + type: LinearSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 20 + upper_limit: 30 + num_samples: 3 + membrane_group: + membrane_cost: + type: LinearSample + param: fs.costing.reverse_osmosis.membrane_cost + lower_limit: 25 + upper_limit: 30 + num_samples: 2 + factor_membrane_replacement: + type: LinearSample + param: fs.costing.reverse_osmosis.factor_membrane_replacement + lower_limit: 0.15 + upper_limit: 0.2 + num_samples: 2 \ No newline at end of file diff --git a/watertap/tools/parallel/parallel_manager_factory.py b/watertap/tools/parallel/parallel_manager_factory.py index eaed8c6ec5..43174be0a2 100644 --- a/watertap/tools/parallel/parallel_manager_factory.py +++ b/watertap/tools/parallel/parallel_manager_factory.py @@ -73,6 +73,13 @@ def has_mpi_peer_processes(): return mpi4py_available and MPI.COMM_WORLD.Get_size() > 1 +def get_mpi_comm_process(): + """ + Returns mpi comm world + """ + return MPI.COMM_WORLD + + def should_fan_out(number_of_subprocesses): """ Returns whether the manager should fan out the computation to subprocesses. This diff --git a/watertap/tools/parameter_sweep/model_manager.py b/watertap/tools/parameter_sweep/model_manager.py index e7f18be705..38d02013cd 100644 --- a/watertap/tools/parameter_sweep/model_manager.py +++ b/watertap/tools/parameter_sweep/model_manager.py @@ -32,7 +32,7 @@ def __init__(self, ps_instance): self.initialized_states = {"state": [], "local_value_k": []} self.current_k = None - def build_and_init(self, params=None, local_value_k=None): + def build_and_init(self, sweep_params=None, local_value_k=None): """build and init model, if required by user, update paramaters before init""" self.model = self.ps_conf.build_model(**self.ps_conf.build_model_kwargs) # intilized model if init function is passed in @@ -40,14 +40,12 @@ def build_and_init(self, params=None, local_value_k=None): # update paramters before init if enabled by user if ( self.ps_conf.update_sweep_params_before_init - and params != None + and sweep_params != None and local_value_k != None ): self.update_model_params(sweep_params, local_value_k) # init - self.ps_conf.initialize_function( - self.model, **self.ps_conf.initialize_kwargs - ) + self.init_model() # raise error if user sets to init before sweep, but does not provide # initilize function elif self.ps_conf.update_sweep_params_before_init: diff --git a/watertap/tools/parameter_sweep/parameter_sweep.py b/watertap/tools/parameter_sweep/parameter_sweep.py index 2f5581e7d9..55e1eb32e6 100644 --- a/watertap/tools/parameter_sweep/parameter_sweep.py +++ b/watertap/tools/parameter_sweep/parameter_sweep.py @@ -693,7 +693,7 @@ def _do_param_sweep(self, sweep_params, outputs, local_values): # build and init model, we also pass first set of paramters incase user wants # to update them before initlizeing the model self.model_manager.build_and_init( - params=sweep_params, local_value_k=local_values[0, :] + sweep_params=sweep_params, local_value_k=local_values[0, :] ) local_num_cases = np.shape(local_values)[0] @@ -1098,7 +1098,7 @@ def parameter_sweep( model = build_model(**build_model_kwargs) sweep_params = build_sweep_params(model, **build_sweep_params_kwargs) sweep_params, sampling_type = self._process_sweep_params(sweep_params) - outputs = build_outputs(model, **build_model_kwargs) + outputs = build_outputs(model, **build_outputs_kwargs) # Set the seed before sampling np.random.seed(seed) diff --git a/watertap/tools/parameter_sweep/parameter_sweep_reader.py b/watertap/tools/parameter_sweep/parameter_sweep_reader.py index b3e38bf80f..b3017d1583 100644 --- a/watertap/tools/parameter_sweep/parameter_sweep_reader.py +++ b/watertap/tools/parameter_sweep/parameter_sweep_reader.py @@ -90,8 +90,70 @@ def get_sweep_params_from_yaml(self, m, yaml_filename): return self._dict_to_params(m, input_dict) @staticmethod - def _dict_to_params(m, input_dict): + def _dict_to_diff_spec(m, input_dict): + """Reads and stores a yaml file as a dictionary + Args: + dict (str): + The dictionary of paramters that are turned into paramter sweep samples + Returns: + input_dict (dict): + The result of reading the yaml file and translating + its structure into a dictionary. + """ + diff_spec = {} + + for param, values in input_dict.items(): + # Find the specified component on the model + component = m.find_component(values["param"]) + + if component is None: + raise ValueError(f'Could not acccess attribute {values["param"]}') + if values["diff_sample_type"] == "NormalSample": + diff_spec[param] = { + "diff_mode": values["diff_mode"], + "diff_sample_type": NormalSample, + "std_dev": values["std_dev"], + "pyomo_object": component, + } + + elif values["diff_sample_type"] == "UniformSample": + if values["diff_mode"] == "percentile": + nominal_lb = values["nominal_lb"] + nominal_ub = values["nominal_ub"] + elif values["diff_mode"] == "sum" or values["diff_mode"] == "product": + nominal_lb = None + nominal_ub = None + diff_spec[param] = { + "diff_mode": values["diff_mode"], + "diff_sample_type": UniformSample, + "relative_lb": values["relative_lb"], + "relative_ub": values["relative_ub"], + "nominal_lb": nominal_lb, + "nominal_ub": nominal_ub, + "pyomo_object": component, + } + elif values["diff_sample_type"] == "LinearSample": + if values["diff_mode"] == "percentile": + nominal_lb = values["nominal_lb"] + nominal_ub = values["nominal_ub"] + elif values["diff_mode"] == "sum" or values["diff_mode"] == "product": + nominal_lb = None + nominal_ub = None + diff_spec[param] = { + "diff_mode": values["diff_mode"], + "diff_sample_type": LinearSample, + "relative_lb": values["relative_lb"], + "relative_ub": values["relative_ub"], + "nominal_lb": nominal_lb, + "nominal_ub": nominal_ub, + "pyomo_object": component, + } + + return diff_spec + + @staticmethod + def _dict_to_params(m, input_dict): """Reads and stores a yaml file as a dictionary Args: @@ -107,7 +169,6 @@ def _dict_to_params(m, input_dict): sweep_params = {} for param, values in input_dict.items(): - # Find the specified component on the model component = m.find_component(values["param"]) @@ -194,7 +255,6 @@ def set_defaults_from_yaml(self, m, yaml_filename, verbose=False): self._set_values_from_dict(m, input_dict, verbose) def _set_values_from_dict(self, m, input_dict, verbose=False): - fail_count = 0 for key, default_value in input_dict.items(): diff --git a/watertap/tools/parameter_sweep/tests/test_parameter_sweep_reader.py b/watertap/tools/parameter_sweep/tests/test_parameter_sweep_reader.py index afac87de12..f124d94377 100644 --- a/watertap/tools/parameter_sweep/tests/test_parameter_sweep_reader.py +++ b/watertap/tools/parameter_sweep/tests/test_parameter_sweep_reader.py @@ -200,3 +200,81 @@ def test_set_defaults_from_yaml_error(self, model, get_default_yaml_file_error): psr.set_defaults_from_yaml(m, filename) os.remove(filename) + + @pytest.mark.unit + def test_dict_to_diff(self, model): + diff_spec_dict = { + "fs.x": { + "diff_mode": "percentile", + "diff_sample_type": "UniformSample", + "param": "fs.x", + "relative_lb": -0.01, + "relative_ub": -0.01, + "nominal_lb": 15, + "nominal_ub": 30, + "num_samples": 1, + }, + } + diff_spec = ParameterSweepReader()._dict_to_diff_spec(model, diff_spec_dict) + for key in diff_spec_dict: + assert key in diff_spec + diff_spec_dict = { + "fs.x": { + "diff_mode": "percentile", + "diff_sample_type": "LinearSample", + "param": "fs.x", + "relative_lb": -0.01, + "relative_ub": -0.01, + "nominal_lb": 15, + "nominal_ub": 30, + "num_samples": 1, + }, + } + diff_spec = ParameterSweepReader()._dict_to_diff_spec(model, diff_spec_dict) + for key in diff_spec_dict: + assert key in diff_spec + diff_spec_dict = { + "fs.x": { + "diff_mode": "sum", + "diff_sample_type": "UniformSample", + "param": "fs.x", + "relative_lb": -0.01, + "relative_ub": -0.01, + "nominal_lb": 15, + "nominal_ub": 30, + "num_samples": 1, + }, + } + diff_spec = ParameterSweepReader()._dict_to_diff_spec(model, diff_spec_dict) + diff_spec_dict["fs.x"]["relative_lb"] = None + diff_spec_dict["fs.x"]["relative_ub"] = None + for key in diff_spec_dict: + assert key in diff_spec + diff_spec_dict = { + "fs.x": { + "diff_mode": "sum", + "diff_sample_type": "LinearSample", + "param": "fs.x", + "relative_lb": -0.01, + "relative_ub": -0.01, + "nominal_lb": 15, + "nominal_ub": 30, + "num_samples": 1, + }, + } + diff_spec = ParameterSweepReader()._dict_to_diff_spec(model, diff_spec_dict) + diff_spec_dict["fs.x"]["relative_lb"] = None + diff_spec_dict["fs.x"]["relative_ub"] = None + for key in diff_spec_dict: + assert key in diff_spec + diff_spec_dict = { + "fs.x": { + "diff_mode": "percentile", + "diff_sample_type": "NormalSample", + "param": "fs.x", + "std_dev": 0.4, + }, + } + diff_spec = ParameterSweepReader()._dict_to_diff_spec(model, diff_spec_dict) + for key in diff_spec_dict: + assert key in diff_spec