Example 3: Sensitivity analysis for a NetLogo model with SALib and Multiprocessing

This is a short demo similar to example two but using the multiprocessing Pool All files used in the example are available from the pyNetLogo repository at https://github.com/quaquel/pyNetLogo. This code requires python3.

For in depth discussion, please see example 2.

Running the experiments in parallel using a Process Pool

There are multiple libraries available in the python ecosystem for performing tasks in parallel. One of the default libraries that ships with Python is concurrent.futures. This is in fact a high level interface around several other libraries. See the documentation for details. One of the libraries wrapped by concurrent.futures is multiprocessing. Below we use multiprocessing, anyone on python 3.8 or newer can use the either code below or use the ProcessPoolExecuturor from concurrent.futures (recommended).

Here we are going to use the ProcessPoolExecutor, which uses the multiprocessing library. Parallelization is an advanced topic and the exact way in which it is to be done depends at least in part on the operating system one is using. It is recommended to carefully read the documentation provided by both concurrent.futures and mulitprocessing. This example is ran on a mac, linux is expected to be similar but Windows is likely to be slightly different

from multiprocessing import Pool
import os
import pandas as pd

import pynetlogo

from SALib.sample import sobol as sobolsample

def initializer(modelfile):
    """initialize a subprocess

    Parameters
    ----------
    modelfile : str

    """

    # we need to set the instantiated netlogo
    # link as a global so run_simulation can
    # use it
    global netlogo

    netlogo = pynetlogo.NetLogoLink(gui=False)
    netlogo.load_model(modelfile)


def run_simulation(experiment):
    """run a netlogo model

    Parameters
    ----------
    experiments : dict

    """

    # Set the input parameters
    for key, value in experiment.items():
        if key == "random-seed":
            # The NetLogo random seed requires a different syntax
            netlogo.command("random-seed {}".format(value))
        else:
            # Otherwise, assume the input parameters are global variables
            netlogo.command("set {0} {1}".format(key, value))

    netlogo.command("setup")
    # Run for 100 ticks and return the number of sheep and
    # wolf agents at each time step
    counts = netlogo.repeat_report(["count sheep", "count wolves"], 100)

    results = pd.Series(
        [counts["count sheep"].values.mean(), counts["count wolves"].values.mean()],
        index=["Avg. sheep", "Avg. wolves"],
    )
    return results


if __name__ == "__main__":
    modelfile = os.path.abspath("./models/Wolf Sheep Predation_v6.nlogo")

    problem = {
        "num_vars": 6,
        "names": [
            "random-seed",
            "grass-regrowth-time",
            "sheep-gain-from-food",
            "wolf-gain-from-food",
            "sheep-reproduce",
            "wolf-reproduce",
        ],
        "bounds": [[1, 100000], [20.0, 40.0], [2.0, 8.0], [16.0, 32.0], [2.0, 8.0], [2.0, 8.0]],
    }

    n = 1024
    param_values = sobolsample.sample(problem, n, calc_second_order=True)

    # cast the param_values to a dataframe to
    # include the column labels
    experiments = pd.DataFrame(param_values, columns=problem["names"])

    with Pool(4, initializer=initializer, initargs=(modelfile,)) as executor:
        results = []
        for entry in executor.map(run_simulation, experiments.to_dict("records")):
            results.append(entry)
        results = pd.DataFrame(results)
[ ]: