How to Run Differential Parameter Sweep

Overview

This guide explains how to run a differential parameter sweep for conducting, e.g., value of innovation (VOI), analysis.

How To

As before, we begin by importing or explicitly programming any functions relating to flowsheet building/specification, simulation, and optimization setup steps. We will use the same RO with energy recovery flowsheet for this example.

# replace this with your own flowsheet module, e.g.
# import my_flowsheet_module as mfm
import watertap.examples.flowsheets.RO_with_energy_recovery.RO_with_energy_recovery as RO_flowsheet

Once this is done, we import the differential parameter sweep tool and sampling classes.

from watertap.tools.parameter_sweep import differential_parameter_sweep, UniformSample, NormalSample

We will use the same setup steps as before to set up the generating functions for our model, sweep params, and outputs:

def build_model(**kwargs):

    # replace these function calls with
    # those in your own flowsheet module

    # set up system
    m = RO_flowsheet.build()
    RO_flowsheet.set_operating_conditions(m)
    RO_flowsheet.initialize_system(m)

    # simulate
    RO_flowsheet.solve(m)

    # set up the model for optimization
    RO_flowsheet.optimize_set_up(m)

    return m

Once the model has been setup, we specify the variables to sample using a dictionary

def build_sweep_params(model, num_samples=5):
    sweep_params = dict()
    sweep_params["A_comp"] = UniformSample(model.fs.RO.A_comp, 4.2e-12, 2.1e-11, num_samples)
    sweep_params["membrane_cost"] = UniformSample(
        model.fs.costing.reverse_osmosis.membrane_cost, 30, 10, num_samples
    )
    return sweep_params

where the A_comp attribute will be randomly selected from a uniform distribution of values in the range \([4.2e-12, 2.1e-11]\) and membrane_cost will be drawn from a uniform distribution between \([30, 10]\). For this example, we will extract flowsheet outputs associated with cost, the levelized cost of water (LCOW) and energy consumption (EC), defined via another dictionary. Next we will construct a specification dictionary to run the differential parameter sweep.

def build_diff_sweep_param_specs(model):
    differential_sweep_specs = dict()

    differential_sweep_specs["A_comp"] = {
        "diff_sample_type": NormalSample,
        "std_dev": 0.3e-12,
        "pyomo_object": model.fs.RO.A_comp,
    }

    differential_sweep_specs["membrane_cost"] = {
        "diff_sample_type": UniformSample,
        "diff_mode": "percentile",
        "nominal_lb" : sweep_params["membrane_cost"].lower_limit,
        "nominal_ub" : sweep_params["membrane_cost"].upper_limit,
        "relative_lb" : -0.05,
        "relative_ub" : -0.05,
        "pyomo_object": model.fs.costing.reverse_osmosis.membrane_cost,
    }

    return differential_sweep_specs

differential_sweep_specs is a specification dictionary that contains details for how to construct the parameter sweep dictionary for differential sweep. This is a nested dictionary where the first level denotes the variable names for which the differential sweep needs to be carried out. The second level denotes various options to be used for each variable. The number of samples for each differential sweep is specified while initializing the DifferentialParameterSweep object using the keyword num_diff_samples. There are 4 modes of setting up a variable to undergo differential sweep:

  1. NormalSample : Uses the nominal value as the mean and expects std_dev key for the differential sweep sampling. It looks like the following:

    differential_sweep_specs["A_comp"] = {
            "diff_sample_type": NormalSample,
            "std_dev": 0.3e-12,
            "pyomo_object": model.fs.RO.A_comp,
        }
    

    This differential mode is unique to variables that expect normal sampling. All other sampling types expect one of the other 3 differential modes below.

  2. sum : Perturbs the nominal value by a certain absolute percentage to create an upper and lower bound for the differential solve. The logic in the code looks as follows:

    lower_bound = nominal_val * (1 - relative_lb)
    upper_bound = nominal_val * (1 + relative_ub)
    
  3. product: Perturbs the nominal value by a scaling factor to create upper and lower bounds for the differential sweep. It uses the following logic

    lower_bound = nominal_val * relative_lb
    upper_bound = nominal_val * relative_ub
    
  4. percentile: Perturbs the nominal value by a percentage of the difference between the nominal upper and lower bound values. The logic is

    delta_nominal = abs(upper_nominal - lower_nominal)
    lower_bound = nominal_val + delta_nominal * relative_lb
    upper_bound = nominal_val + delta_nominal * relative_ub
    

An example differential sweep spec dictionary may look like the following:

differential_sweep_specs = dict()
differential_sweep_specs["membrane_cost"] = {
        "diff_sample_type": UniformSample,
        "diff_mode": "percentile",
        "nominal_lb" : sweep_params["membrane_cost"].lower_limit,
        "nominal_ub" : sweep_params["membrane_cost"].upper_limit,
        "relative_lb" : -0.05,
        "relative_ub" : -0.05,
        "pyomo_object": model.fs.costing.reverse_osmosis.membrane_cost,
    }
differential_sweep_specs["px_cost"] = {
    "diff_sample_type": LinearSample,
    "diff_mode": "sum",
    "relative_lb" : -0.05,
    "relative_ub" : -0.05,
    "pyomo_object": m.fs.costing.pressure_exchanger.cost,
}
differential_sweep_specs["px_efficiency"] = {
    "diff_sample_type": UniformSample,
    "diff_mode": "product",
    "relative_lb" : 0.001,
    "relative_ub" : 0.001,
    "pyomo_object": m.fs.PXR.efficiency_pressure_exchanger,
}

Important

The user can only conduct differential sweeps for variables specified with sweep_params.

Continuing with the example test code from above, we will use the following function for building the outputs.

def build_outputs(model, sweep_params):
    outputs = dict()
    outputs['EC'] = model.fs.costing.specific_energy_consumption
    outputs['LCOW'] = model.fs.costing.LCOW
    return outputs

With the flowsheet defined and suitably initialized, along with the definitions for sweep_params, differential_sweep_specs, and outputs on hand, we can call the differential_parameter_sweep function as before.

Note

This documentation currently uses the older API for calling the differential parameter sweep. This API will be deprecated in the near future. The documentation will be changed to reflect this accordingly. We recommend running the differential parameter sweep in serial or with MPI only.

# Define the local results directory, num_samples, and seed (if desired)
num_samples = 5
seed = None

model = build_model()
sweep_params = build_sweep_params(model, num_samples=num_samples)
differential_sweep_specs = build_diff_sweep_param_specs(model)
outputs = build_outputs(model, sweep_params)

# Run the parameter sweep
global_results = differential_parameter_sweep(
        build_model,
        build_sweep_params,
        differential_sweep_specs,
        outputs,
        h5_results_file_name='monte_carlo_results.h5',
        optimize_function=RO_flowsheet.optimize,
        debugging_data_dir=None,
        num_samples=num_samples,
        num_diff_samples=2,
        seed=seed,
    )
...

Module Documentation