example_mpi_lake_model.py

  1"""An example of the lake problem using the ema workbench with the MPI evaluator.
  2
  3The model itself is adapted from the Rhodium example by Dave Hadka,
  4see https://gist.github.com/dhadka/a8d7095c98130d8f73bc
  5
  6"""
  7
  8import os
  9import sys
 10
 11sys.path.insert(1, os.path.abspath("../../"))
 12
 13import math
 14import time
 15
 16import numpy as np
 17from scipy.optimize import brentq
 18
 19from ema_workbench import (
 20    Constant,
 21    Model,
 22    MPIEvaluator,
 23    RealParameter,
 24    ScalarOutcome,
 25    ema_logging,
 26    save_results,
 27)
 28
 29
 30def lake_problem(
 31    b=0.42,  # decay rate for P in lake (0.42 = irreversible)
 32    q=2.0,  # recycling exponent
 33    mean=0.02,  # mean of natural inflows
 34    stdev=0.001,  # future utility discount rate
 35    delta=0.98,  # standard deviation of natural inflows
 36    alpha=0.4,  # utility from pollution
 37    nsamples=100,  # Monte Carlo sampling of natural inflows
 38    **kwargs,
 39):
 40    """Intertemporal version of the lake problem."""
 41    try:
 42        decisions = [kwargs[str(i)] for i in range(100)]
 43    except KeyError:
 44        decisions = [0] * 100
 45        print("No valid decisions found, using 0 water release every year as default")
 46
 47    n_vars = len(decisions)
 48    decisions = np.array(decisions)
 49
 50    # Calculate the critical pollution level ({_crit)
 51    p_crit = brentq(lambda x: x**q / (1 + x**q) - b * x, 0.01, 1.5)
 52
 53    # Generate natural inflows using lognormal distribution
 54    natural_inflows = np.random.lognormal(
 55        mean=math.log(mean**2 / math.sqrt(stdev**2 + mean**2)),
 56        sigma=math.sqrt(math.log(1.0 + stdev**2 / mean**2)),
 57        size=(nsamples, n_vars),
 58    )
 59
 60    # Initialize the pollution level matrix X
 61    x = np.zeros((nsamples, n_vars))
 62
 63    # Loop through time to compute the pollution levels
 64    for t in range(1, n_vars):
 65        x[:, t] = (
 66            (1 - b) * x[:, t - 1]
 67            + (x[:, t - 1] ** q / (1 + x[:, t - 1] ** q))
 68            + decisions[t - 1]
 69            + natural_inflows[:, t - 1]
 70        )
 71
 72    # Calculate the average daily pollution for each time step
 73    average_daily_p = np.mean(x, axis=0)
 74
 75    # Calculate the reliability (probability of the pollution level being below {_crit)
 76    reliability = np.sum(p_crit > x) / float(nsamples * n_vars)
 77
 78    # Calculate the maximum pollution level (max_p)
 79    max_p = np.max(average_daily_p)
 80
 81    # Calculate the utility by discounting the decisions using the discount factor (delta)
 82    utility = np.sum(alpha * decisions * np.power(delta, np.arange(n_vars)))
 83
 84    # Calculate the inertia (the fraction of time steps with changes larger than 0.02)
 85    inertia = np.sum(np.abs(np.diff(decisions)) > 0.02) / float(n_vars - 1)
 86
 87    return max_p, utility, inertia, reliability
 88
 89
 90if __name__ == "__main__":
 91    import ema_workbench
 92
 93    # run with env MPI4PY_FUTURES_MAX_WORKERS=4 mpiexec -n 1 python example_mpi_lake_model.py
 94    starttime = time.perf_counter()
 95
 96    ema_logging.log_to_stderr(ema_logging.INFO, pass_root_logger_level=True)
 97    ema_logging.get_rootlogger().info(f"{ema_workbench.__version__}")
 98
 99    # instantiate the model
100    lake_model = Model("lakeproblem", function=lake_problem)
101    lake_model.time_horizon = 100
102
103    # specify uncertainties
104    lake_model.uncertainties = [
105        RealParameter("b", 0.1, 0.45),
106        RealParameter("q", 2.0, 4.5),
107        RealParameter("mean", 0.01, 0.05),
108        RealParameter("stdev", 0.001, 0.005),
109        RealParameter("delta", 0.93, 0.99),
110    ]
111
112    # set levers, one for each time step
113    lake_model.levers = [
114        RealParameter(str(i), 0, 0.1) for i in range(lake_model.time_horizon)
115    ]
116
117    # specify outcomes
118    lake_model.outcomes = [
119        ScalarOutcome("max_P"),
120        ScalarOutcome("utility"),
121        ScalarOutcome("inertia"),
122        ScalarOutcome("reliability"),
123    ]
124
125    # override some of the defaults of the model
126    lake_model.constants = [Constant("alpha", 0.41), Constant("n_samples", 150)]
127
128    # generate some random policies by sampling over levers
129    n_scenarios = 10000
130    n_policies = 4
131
132    with MPIEvaluator(lake_model) as evaluator:
133        res = evaluator.perform_experiments(n_scenarios, n_policies, chunksize=250)
134
135    save_results(res, f"mpi_{n_scenarios}-scen-{n_policies}-policies-results.tar.gz")
136
137    print(time.perf_counter() - starttime)